Skip to content

Commit 3e7861f

Browse files
committed
STAC-23483: Creating a helper function for port-forwarding
1 parent 9d55cd7 commit 3e7861f

7 files changed

Lines changed: 198 additions & 44 deletions

File tree

cmd/elasticsearch/configure.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/spf13/cobra"
8+
"github.com/stackvista/stackstate-backup-cli/cmd/portforward"
89
"github.com/stackvista/stackstate-backup-cli/internal/config"
910
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
1011
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
@@ -51,21 +52,14 @@ func runConfigure(cliCtx *config.Context) error {
5152
localPort := cfg.Elasticsearch.Service.LocalPortForwardPort
5253
remotePort := cfg.Elasticsearch.Service.Port
5354

54-
log.Infof("Setting up port-forward to %s:%d in namespace %s...", serviceName, remotePort, cliCtx.Config.Namespace)
55-
56-
stopChan, readyChan, err := k8sClient.PortForwardService(cliCtx.Config.Namespace, serviceName, localPort, remotePort)
55+
pf, err := portforward.SetupPortForward(k8sClient, cliCtx.Config.Namespace, serviceName, localPort, remotePort, log)
5756
if err != nil {
58-
return fmt.Errorf("failed to setup port-forward: %w", err)
57+
return err
5958
}
60-
defer close(stopChan)
61-
62-
// Wait for port-forward to be ready
63-
<-readyChan
64-
65-
log.Successf("Port-forward established successfully")
59+
defer close(pf.StopChan)
6660

6761
// Create Elasticsearch client
68-
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", localPort))
62+
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", pf.LocalPort))
6963
if err != nil {
7064
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
7165
}

cmd/elasticsearch/list-indices.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/spf13/cobra"
8+
"github.com/stackvista/stackstate-backup-cli/cmd/portforward"
89
"github.com/stackvista/stackstate-backup-cli/internal/config"
910
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
1011
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
@@ -46,21 +47,14 @@ func runListIndices(cliCtx *config.Context) error {
4647
localPort := cfg.Elasticsearch.Service.LocalPortForwardPort
4748
remotePort := cfg.Elasticsearch.Service.Port
4849

49-
log.Infof("Setting up port-forward to %s:%d in namespace %s...", serviceName, remotePort, cliCtx.Config.Namespace)
50-
51-
stopChan, readyChan, err := k8sClient.PortForwardService(cliCtx.Config.Namespace, serviceName, localPort, remotePort)
50+
pf, err := portforward.SetupPortForward(k8sClient, cliCtx.Config.Namespace, serviceName, localPort, remotePort, log)
5251
if err != nil {
53-
return fmt.Errorf("failed to setup port-forward: %w", err)
52+
return err
5453
}
55-
defer close(stopChan)
56-
57-
// Wait for port-forward to be ready
58-
<-readyChan
59-
60-
log.Successf("Port-forward established successfully")
54+
defer close(pf.StopChan)
6155

6256
// Create Elasticsearch client
63-
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", localPort))
57+
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", pf.LocalPort))
6458
if err != nil {
6559
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
6660
}

cmd/elasticsearch/list-snapshots.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/spf13/cobra"
8+
"github.com/stackvista/stackstate-backup-cli/cmd/portforward"
89
"github.com/stackvista/stackstate-backup-cli/internal/config"
910
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
1011
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
@@ -46,21 +47,14 @@ func runListSnapshots(cliCtx *config.Context) error {
4647
localPort := cfg.Elasticsearch.Service.LocalPortForwardPort
4748
remotePort := cfg.Elasticsearch.Service.Port
4849

49-
log.Infof("Setting up port-forward to %s:%d in namespace %s...", serviceName, remotePort, cliCtx.Config.Namespace)
50-
51-
stopChan, readyChan, err := k8sClient.PortForwardService(cliCtx.Config.Namespace, serviceName, localPort, remotePort)
50+
pf, err := portforward.SetupPortForward(k8sClient, cliCtx.Config.Namespace, serviceName, localPort, remotePort, log)
5251
if err != nil {
53-
return fmt.Errorf("failed to setup port-forward: %w", err)
52+
return err
5453
}
55-
defer close(stopChan)
56-
57-
// Wait for port-forward to be ready
58-
<-readyChan
59-
60-
log.Successf("Port-forward established successfully")
54+
defer close(pf.StopChan)
6155

6256
// Create Elasticsearch client
63-
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", localPort))
57+
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", pf.LocalPort))
6458
if err != nil {
6559
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
6660
}

cmd/elasticsearch/restore-snapshot.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/spf13/cobra"
11+
"github.com/stackvista/stackstate-backup-cli/cmd/portforward"
1112
"github.com/stackvista/stackstate-backup-cli/internal/config"
1213
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
1314
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
@@ -90,21 +91,14 @@ func runRestore(cliCtx *config.Context) error {
9091
localPort := cfg.Elasticsearch.Service.LocalPortForwardPort
9192
remotePort := cfg.Elasticsearch.Service.Port
9293

93-
log.Infof("Setting up port-forward to %s:%d in namespace %s...", serviceName, remotePort, cliCtx.Config.Namespace)
94-
95-
stopChan, readyChan, err := k8sClient.PortForwardService(cliCtx.Config.Namespace, serviceName, localPort, remotePort)
94+
pf, err := portforward.SetupPortForward(k8sClient, cliCtx.Config.Namespace, serviceName, localPort, remotePort, log)
9695
if err != nil {
97-
return fmt.Errorf("failed to setup port-forward: %w", err)
96+
return err
9897
}
99-
defer close(stopChan)
100-
101-
// Wait for port-forward to be ready
102-
<-readyChan
103-
104-
log.Successf("Port-forward established successfully")
98+
defer close(pf.StopChan)
10599

106100
// Create Elasticsearch client
107-
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", localPort))
101+
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", pf.LocalPort))
108102
if err != nil {
109103
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
110104
}

cmd/portforward/portforward.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package portforward
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
7+
"github.com/stackvista/stackstate-backup-cli/internal/logger"
8+
)
9+
10+
// Conn contains the channels needed to manage a port-forward connection
11+
type Conn struct {
12+
StopChan chan struct{}
13+
ReadyChan <-chan struct{}
14+
LocalPort int
15+
}
16+
17+
// SetupPortForward establishes a port-forward to a Kubernetes service and waits for it to be ready.
18+
// It returns a Conn containing the stop and ready channels, plus the local port.
19+
// The caller is responsible for closing the StopChan when done.
20+
func SetupPortForward(
21+
k8sClient *k8s.Client,
22+
namespace string,
23+
serviceName string,
24+
localPort int,
25+
remotePort int,
26+
log *logger.Logger,
27+
) (*Conn, error) {
28+
log.Infof("Setting up port-forward to %s:%d in namespace %s...", serviceName, remotePort, namespace)
29+
30+
stopChan, readyChan, err := k8sClient.PortForwardService(namespace, serviceName, localPort, remotePort)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to setup port-forward: %w", err)
33+
}
34+
35+
// Wait for port-forward to be ready
36+
<-readyChan
37+
38+
log.Successf("Port-forward established successfully")
39+
40+
return &Conn{
41+
StopChan: stopChan,
42+
ReadyChan: readyChan,
43+
LocalPort: localPort,
44+
}, nil
45+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package portforward
2+
3+
import (
4+
"testing"
5+
6+
corev1 "k8s.io/api/core/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/client-go/kubernetes/fake"
9+
10+
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
11+
"github.com/stackvista/stackstate-backup-cli/internal/logger"
12+
)
13+
14+
func TestSetupPortForward_ServiceNotFound(t *testing.T) {
15+
fakeClientset := fake.NewSimpleClientset()
16+
client := k8s.NewTestClient(fakeClientset)
17+
log := logger.New(true, false)
18+
19+
_, err := SetupPortForward(client, "default", "nonexistent-service", 8080, 9200, log)
20+
if err == nil {
21+
t.Fatal("expected error for nonexistent service, got nil")
22+
}
23+
}
24+
25+
func TestSetupPortForward_NoPodsFound(t *testing.T) {
26+
fakeClientset := fake.NewSimpleClientset(
27+
&corev1.Service{
28+
ObjectMeta: metav1.ObjectMeta{
29+
Name: "test-service",
30+
Namespace: "default",
31+
},
32+
Spec: corev1.ServiceSpec{
33+
Selector: map[string]string{
34+
"app": "test",
35+
},
36+
},
37+
},
38+
)
39+
client := k8s.NewTestClient(fakeClientset)
40+
log := logger.New(true, false)
41+
42+
_, err := SetupPortForward(client, "default", "test-service", 8080, 9200, log)
43+
if err == nil {
44+
t.Fatal("expected error for service with no pods, got nil")
45+
}
46+
}
47+
48+
func TestSetupPortForward_NoRunningPods(t *testing.T) {
49+
fakeClientset := fake.NewSimpleClientset(
50+
&corev1.Service{
51+
ObjectMeta: metav1.ObjectMeta{
52+
Name: "test-service",
53+
Namespace: "default",
54+
},
55+
Spec: corev1.ServiceSpec{
56+
Selector: map[string]string{
57+
"app": "test",
58+
},
59+
},
60+
},
61+
&corev1.Pod{
62+
ObjectMeta: metav1.ObjectMeta{
63+
Name: "test-pod",
64+
Namespace: "default",
65+
Labels: map[string]string{
66+
"app": "test",
67+
},
68+
},
69+
Status: corev1.PodStatus{
70+
Phase: corev1.PodPending,
71+
},
72+
},
73+
)
74+
client := k8s.NewTestClient(fakeClientset)
75+
log := logger.New(true, false)
76+
77+
_, err := SetupPortForward(client, "default", "test-service", 8080, 9200, log)
78+
if err == nil {
79+
t.Fatal("expected error for service with no running pods, got nil")
80+
}
81+
}
82+
83+
func TestConn_Structure(t *testing.T) {
84+
stopChan := make(chan struct{})
85+
readyChan := make(chan struct{})
86+
localPort := 8080
87+
88+
result := &Conn{
89+
StopChan: stopChan,
90+
ReadyChan: readyChan,
91+
LocalPort: localPort,
92+
}
93+
94+
if result.StopChan == nil {
95+
t.Error("expected StopChan to be set")
96+
}
97+
if result.ReadyChan == nil {
98+
t.Error("expected ReadyChan to be set")
99+
}
100+
if result.LocalPort != localPort {
101+
t.Errorf("expected LocalPort to be %d, got %d", localPort, result.LocalPort)
102+
}
103+
}
104+
105+
func TestConn_ChannelCleanup(t *testing.T) {
106+
stopChan := make(chan struct{})
107+
readyChan := make(chan struct{})
108+
109+
result := &Conn{
110+
StopChan: stopChan,
111+
ReadyChan: readyChan,
112+
LocalPort: 8080,
113+
}
114+
115+
close(result.StopChan)
116+
117+
select {
118+
case <-result.StopChan:
119+
// Successfully received from closed channel
120+
default:
121+
t.Error("expected StopChan to be closed")
122+
}
123+
}

internal/k8s/client.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,3 +219,13 @@ func (c *Client) ScaleUpDeployments(namespace string, deploymentScales []Deploym
219219

220220
return nil
221221
}
222+
223+
// NewTestClient creates a k8s Client for testing with a fake clientset.
224+
// This function is exported so it can be used in other package tests.
225+
func NewTestClient(clientset kubernetes.Interface) *Client {
226+
return &Client{
227+
clientset: clientset,
228+
restConfig: nil,
229+
debug: false,
230+
}
231+
}

0 commit comments

Comments
 (0)