Skip to content

Commit 45338eb

Browse files
committed
STAC-24392: Dynamic Port Allocation for Port-Forwarding
1 parent c0d9a3e commit 45338eb

File tree

31 files changed

+261
-193
lines changed

31 files changed

+261
-193
lines changed

.opencode/agents/code-reviewer.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ Commands MUST use `app.Context` for dependencies, NOT create clients directly:
108108
// GOOD
109109
func runRestore(appCtx *app.Context) error {
110110
appCtx.K8sClient // Use injected client
111-
appCtx.ESClient
112111
appCtx.Config
113112
appCtx.Logger
114113
appCtx.Formatter
114+
// Service clients created via factory methods after port-forwarding
115+
esClient, err := appCtx.NewESClient(pf.LocalPort)
115116
}
116117

117118
// BAD - Direct client creation in command

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ internal/
137137
// GOOD
138138
func runRestore(appCtx *app.Context) error {
139139
appCtx.K8sClient // Kubernetes client
140-
appCtx.ESClient // Elasticsearch client
141140
appCtx.Config // Configuration
142141
appCtx.Logger // Structured logger
142+
// Service clients created via factory methods after port-forwarding
143+
esClient, err := appCtx.NewESClient(pf.LocalPort)
143144
}
144145
```
145146

ARCHITECTURE.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,13 @@ if err != nil {
100100
}
101101

102102
// All dependencies available via appCtx
103-
appCtx.K8sClient
104-
appCtx.S3Client
105-
appCtx.ESClient
106-
appCtx.Config
107-
appCtx.Logger
108-
appCtx.Formatter
103+
appCtx.K8sClient // Kubernetes client
104+
appCtx.Config // Configuration
105+
appCtx.Logger // Structured logger
106+
appCtx.Formatter // Output formatter
107+
appCtx.NewESClient(localPort) // Elasticsearch client factory
108+
appCtx.NewS3Client(localPort) // S3/Minio client factory
109+
appCtx.NewCHClient(backupAPIPort, dbPort) // ClickHouse client factory
109110
```
110111

111112
**Dependency Rules**:
@@ -226,9 +227,10 @@ func runList(appCtx *app.Context) error {
226227
// All dependencies available immediately
227228
appCtx.K8sClient
228229
appCtx.Config
229-
appCtx.S3Client
230230
appCtx.Logger
231231
appCtx.Formatter
232+
// Service clients created via factory methods with port-forwarded port
233+
s3Client, err := appCtx.NewS3Client(pf.LocalPort)
232234
}
233235
```
234236

@@ -455,9 +457,11 @@ func runListSnapshots(globalFlags *config.CLIGlobalFlags) error {
455457
```go
456458
// GOOD
457459
func runListSnapshots(appCtx *app.Context) error {
458-
// Dependencies already created
460+
// Direct dependencies
459461
appCtx.K8sClient
460-
appCtx.ESClient
462+
appCtx.Config
463+
// Service clients created via factory methods after port-forwarding
464+
esClient, err := appCtx.NewESClient(pf.LocalPort)
461465
}
462466
```
463467

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,6 @@ elasticsearch:
319319
service:
320320
name: suse-observability-elasticsearch-master-headless
321321
port: 9200
322-
localPortForwardPort: 9200
323322

324323
restore:
325324
repository: sts-backup

cmd/clickhouse/check_and_finalize.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/spf13/cobra"
88
"github.com/stackvista/stackstate-backup-cli/cmd/cmdutils"
99
"github.com/stackvista/stackstate-backup-cli/internal/app"
10-
"github.com/stackvista/stackstate-backup-cli/internal/clients/clickhouse"
10+
ch "github.com/stackvista/stackstate-backup-cli/internal/clients/clickhouse"
1111
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
1212
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/portforward"
1313
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/restore"
@@ -45,28 +45,34 @@ It will check the restore status and if complete, execute post-restore tasks and
4545
}
4646

4747
func runCheckAndFinalize(appCtx *app.Context) error {
48-
// Setup port-forward
48+
// Setup port-forward to ClickHouse Backup API
4949
pf, err := portforward.SetupPortForward(
5050
appCtx.K8sClient,
5151
appCtx.Namespace,
5252
appCtx.Config.Clickhouse.BackupService.Name,
53-
appCtx.Config.Clickhouse.BackupService.LocalPortForwardPort,
5453
appCtx.Config.Clickhouse.BackupService.Port,
5554
appCtx.Logger,
5655
)
5756
if err != nil {
5857
return err
5958
}
6059
defer close(pf.StopChan)
61-
return checkAndFinalize(appCtx, checkOperationID, waitForRestore)
60+
61+
// Create CH client with backup API port only
62+
chClient, err := appCtx.NewCHClient(pf.LocalPort, 0)
63+
if err != nil {
64+
return fmt.Errorf("failed to create ClickHouse client: %w", err)
65+
}
66+
67+
return checkAndFinalize(chClient, appCtx, checkOperationID, waitForRestore)
6268
}
6369

6470
// checkAndFinalize checks restore status and finalizes if complete
65-
func checkAndFinalize(appCtx *app.Context, operationID string, waitForComplete bool) error {
71+
func checkAndFinalize(chClient ch.Interface, appCtx *app.Context, operationID string, waitForComplete bool) error {
6672
// Check status
6773
appCtx.Logger.Println()
6874
appCtx.Logger.Infof("Checking restore status for operation: %s", operationID)
69-
status, err := appCtx.CHClient.GetRestoreStatus(appCtx.Context, operationID)
75+
status, err := chClient.GetRestoreStatus(appCtx.Context, operationID)
7076
if err != nil {
7177
return err
7278
}
@@ -87,7 +93,7 @@ func checkAndFinalize(appCtx *app.Context, operationID string, waitForComplete b
8793
if waitForComplete {
8894
// Still running - wait
8995
appCtx.Logger.Infof("Restore is in progress, waiting for completion...")
90-
return waitAndFinalize(appCtx, appCtx.CHClient, operationID)
96+
return waitAndFinalize(appCtx, chClient, operationID)
9197
}
9298
// Just print status
9399
appCtx.Logger.Println()
@@ -96,7 +102,7 @@ func checkAndFinalize(appCtx *app.Context, operationID string, waitForComplete b
96102
}
97103

98104
// waitAndFinalize waits for restore completion and finalizes
99-
func waitAndFinalize(appCtx *app.Context, chClient clickhouse.Interface, operationID string) error {
105+
func waitAndFinalize(appCtx *app.Context, chClient ch.Interface, operationID string) error {
100106
restore.PrintAPIWaitingMessage("clickhouse", operationID, appCtx.Namespace, appCtx.Logger)
101107

102108
// Wait for restore using shared utility
@@ -157,7 +163,6 @@ func executePostRestoreSQL(appCtx *app.Context) error {
157163
appCtx.K8sClient,
158164
appCtx.Namespace,
159165
appCtx.Config.Clickhouse.Service.Name,
160-
appCtx.Config.Clickhouse.Service.LocalPortForwardPort,
161166
appCtx.Config.Clickhouse.Service.Port,
162167
appCtx.Logger,
163168
)
@@ -166,8 +171,14 @@ func executePostRestoreSQL(appCtx *app.Context) error {
166171
}
167172
defer close(pf.StopChan)
168173

174+
// Create ClickHouse client with DB port only
175+
chDBClient, err := appCtx.NewCHClient(0, pf.LocalPort)
176+
if err != nil {
177+
return fmt.Errorf("failed to create ClickHouse DB client: %w", err)
178+
}
179+
169180
// Create ClickHouse SQL connection
170-
conn, closeConn, err := appCtx.CHClient.Connect()
181+
conn, closeConn, err := chDBClient.Connect()
171182
if err != nil {
172183
return fmt.Errorf("failed to connect to ClickHouse: %w", err)
173184
}

cmd/clickhouse/list.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ func runList(appCtx *app.Context) error {
2929
appCtx.K8sClient,
3030
appCtx.Namespace,
3131
appCtx.Config.Clickhouse.BackupService.Name,
32-
appCtx.Config.Clickhouse.BackupService.LocalPortForwardPort,
3332
appCtx.Config.Clickhouse.BackupService.Port,
3433
appCtx.Logger,
3534
)
@@ -38,11 +37,17 @@ func runList(appCtx *app.Context) error {
3837
}
3938
defer close(pf.StopChan)
4039

40+
// Create CH client with backup API port only
41+
chClient, err := appCtx.NewCHClient(pf.LocalPort, 0)
42+
if err != nil {
43+
return fmt.Errorf("failed to create ClickHouse client: %w", err)
44+
}
45+
4146
// List backups
4247
appCtx.Logger.Infof("Listing Clickhouse backups...")
4348
appCtx.Logger.Println()
4449

45-
backups, err := appCtx.CHClient.ListBackups(appCtx.Context)
50+
backups, err := chClient.ListBackups(appCtx.Context)
4651
if err != nil {
4752
return fmt.Errorf("failed to list backups: %w", err)
4853
}

cmd/clickhouse/restore.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ func executeRestore(appCtx *app.Context, backupName string, waitForComplete bool
9696
appCtx.K8sClient,
9797
appCtx.Namespace,
9898
appCtx.Config.Clickhouse.BackupService.Name,
99-
appCtx.Config.Clickhouse.BackupService.LocalPortForwardPort,
10099
appCtx.Config.Clickhouse.BackupService.Port,
101100
appCtx.Logger,
102101
)
@@ -105,10 +104,16 @@ func executeRestore(appCtx *app.Context, backupName string, waitForComplete bool
105104
}
106105
defer close(pf.StopChan)
107106

107+
// Create CH client with backup API port only
108+
chClient, err := appCtx.NewCHClient(pf.LocalPort, 0)
109+
if err != nil {
110+
return fmt.Errorf("failed to create ClickHouse client: %w", err)
111+
}
112+
108113
// Trigger restore
109114
appCtx.Logger.Println()
110115
appCtx.Logger.Infof("Triggering restore for backup: %s", backupName)
111-
operationID, err := appCtx.CHClient.TriggerRestore(appCtx.Context, backupName)
116+
operationID, err := chClient.TriggerRestore(appCtx.Context, backupName)
112117
if err != nil {
113118
return fmt.Errorf("failed to trigger restore: %w", err)
114119
}
@@ -119,7 +124,7 @@ func executeRestore(appCtx *app.Context, backupName string, waitForComplete bool
119124
return nil
120125
}
121126

122-
return checkAndFinalize(appCtx, operationID, waitForComplete)
127+
return checkAndFinalize(chClient, appCtx, operationID, waitForComplete)
123128
}
124129

125130
// getLatestBackupForRestore retrieves the most recent backup
@@ -129,7 +134,6 @@ func getLatestBackupForRestore(appCtx *app.Context) (string, error) {
129134
appCtx.K8sClient,
130135
appCtx.Namespace,
131136
appCtx.Config.Clickhouse.BackupService.Name,
132-
appCtx.Config.Clickhouse.BackupService.LocalPortForwardPort,
133137
appCtx.Config.Clickhouse.BackupService.Port,
134138
appCtx.Logger,
135139
)
@@ -138,8 +142,14 @@ func getLatestBackupForRestore(appCtx *app.Context) (string, error) {
138142
}
139143
defer close(pf.StopChan)
140144

145+
// Create CH client with backup API port only
146+
chClient, err := appCtx.NewCHClient(pf.LocalPort, 0)
147+
if err != nil {
148+
return "", fmt.Errorf("failed to create ClickHouse client: %w", err)
149+
}
150+
141151
// List backups
142-
backups, err := appCtx.CHClient.ListBackups(appCtx.Context)
152+
backups, err := chClient.ListBackups(appCtx.Context)
143153
if err != nil {
144154
return "", fmt.Errorf("failed to list backups: %w", err)
145155
}

cmd/elasticsearch/check_and_finalize.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/spf13/cobra"
77
"github.com/stackvista/stackstate-backup-cli/cmd/cmdutils"
88
"github.com/stackvista/stackstate-backup-cli/internal/app"
9+
es "github.com/stackvista/stackstate-backup-cli/internal/clients/elasticsearch"
910
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
1011
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/portforward"
1112
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/restore"
@@ -39,24 +40,29 @@ If the restore is still running and --wait is specified, wait for completion bef
3940
func runCheckAndFinalize(appCtx *app.Context) error {
4041
// Setup port-forward to Elasticsearch
4142
serviceName := appCtx.Config.Elasticsearch.Service.Name
42-
localPort := appCtx.Config.Elasticsearch.Service.LocalPortForwardPort
4343
remotePort := appCtx.Config.Elasticsearch.Service.Port
4444

45-
pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, localPort, remotePort, appCtx.Logger)
45+
pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, remotePort, appCtx.Logger)
4646
if err != nil {
4747
return err
4848
}
4949
defer close(pf.StopChan)
5050

51+
// Create ES client with actual port
52+
esClient, err := appCtx.NewESClient(pf.LocalPort)
53+
if err != nil {
54+
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
55+
}
56+
5157
repository := appCtx.Config.Elasticsearch.Restore.Repository
5258

53-
return checkAndFinalize(appCtx, repository, checkOperationID, checkWait)
59+
return checkAndFinalize(esClient, appCtx, repository, checkOperationID, checkWait)
5460
}
5561

56-
func checkAndFinalize(appCtx *app.Context, repository, snapshotName string, waitForComplete bool) error {
62+
func checkAndFinalize(esClient es.Interface, appCtx *app.Context, repository, snapshotName string, waitForComplete bool) error {
5763
// Get restore status
5864
appCtx.Logger.Infof("Checking restore status for snapshot: %s", snapshotName)
59-
status, isComplete, err := appCtx.ESClient.GetRestoreStatus(repository, snapshotName)
65+
status, isComplete, err := esClient.GetRestoreStatus(repository, snapshotName)
6066
if err != nil {
6167
return fmt.Errorf("failed to get restore status: %w", err)
6268
}
@@ -87,7 +93,7 @@ func checkAndFinalize(appCtx *app.Context, repository, snapshotName string, wait
8793

8894
if waitForComplete {
8995
appCtx.Logger.Println()
90-
return waitAndFinalize(appCtx, repository, snapshotName)
96+
return waitAndFinalize(esClient, appCtx, repository, snapshotName)
9197
}
9298

9399
// Not waiting - print status and exit
@@ -97,12 +103,12 @@ func checkAndFinalize(appCtx *app.Context, repository, snapshotName string, wait
97103
}
98104

99105
// waitAndFinalize waits for restore to complete and finalizes (scale up)
100-
func waitAndFinalize(appCtx *app.Context, repository, snapshotName string) error {
106+
func waitAndFinalize(esClient es.Interface, appCtx *app.Context, repository, snapshotName string) error {
101107
restore.PrintAPIWaitingMessage("elasticsearch", snapshotName, appCtx.Namespace, appCtx.Logger)
102108

103109
// Wait for restore to complete
104110
checkStatusFn := func() (string, bool, error) {
105-
return appCtx.ESClient.GetRestoreStatus(repository, snapshotName)
111+
return esClient.GetRestoreStatus(repository, snapshotName)
106112
}
107113

108114
if err := restore.WaitForAPIRestore(checkStatusFn, 0, appCtx.Logger); err != nil {

cmd/elasticsearch/configure.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,20 @@ func runConfigure(appCtx *app.Context) error {
2929

3030
// Setup port-forward to Elasticsearch
3131
serviceName := appCtx.Config.Elasticsearch.Service.Name
32-
localPort := appCtx.Config.Elasticsearch.Service.LocalPortForwardPort
3332
remotePort := appCtx.Config.Elasticsearch.Service.Port
3433

35-
pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, localPort, remotePort, appCtx.Logger)
34+
pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, remotePort, appCtx.Logger)
3635
if err != nil {
3736
return err
3837
}
3938
defer close(pf.StopChan)
4039

40+
// Create ES client with actual port
41+
esClient, err := appCtx.NewESClient(pf.LocalPort)
42+
if err != nil {
43+
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
44+
}
45+
4146
// Configure snapshot repository
4247
repo := appCtx.Config.Elasticsearch.SnapshotRepository
4348

@@ -50,7 +55,7 @@ func runConfigure(appCtx *app.Context) error {
5055

5156
appCtx.Logger.Infof("Configuring snapshot repository '%s' (bucket: %s)...", repo.Name, repo.Bucket)
5257

53-
err = appCtx.ESClient.ConfigureSnapshotRepository(
58+
err = esClient.ConfigureSnapshotRepository(
5459
repo.Name,
5560
repo.Bucket,
5661
repo.Endpoint,
@@ -68,7 +73,7 @@ func runConfigure(appCtx *app.Context) error {
6873
slm := appCtx.Config.Elasticsearch.SLM
6974
appCtx.Logger.Infof("Configuring SLM policy '%s'...", slm.Name)
7075

71-
err = appCtx.ESClient.ConfigureSLMPolicy(
76+
err = esClient.ConfigureSLMPolicy(
7277
slm.Name,
7378
slm.Schedule,
7479
slm.SnapshotTemplateName,

cmd/elasticsearch/configure_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (m *mockESClientForConfigure) IndexExists(_ string) (bool, error) {
9292
return false, fmt.Errorf("not implemented")
9393
}
9494

95-
func (m *mockESClientForConfigure) RestoreSnapshot(_, _, _ string, _ bool) error {
95+
func (m *mockESClientForConfigure) RestoreSnapshot(_, _, _ string) error {
9696
return fmt.Errorf("not implemented")
9797
}
9898

@@ -138,7 +138,6 @@ elasticsearch:
138138
service:
139139
name: elasticsearch-master
140140
port: 9200
141-
localPortForwardPort: 9200
142141
restore:
143142
scaleDownLabelSelector: app=test
144143
indexPrefix: sts_
@@ -173,7 +172,6 @@ elasticsearch:
173172
service:
174173
name: elasticsearch-master
175174
port: 9200
176-
localPortForwardPort: 9200
177175
restore:
178176
scaleDownLabelSelector: app=test
179177
indexPrefix: sts_

0 commit comments

Comments
 (0)