Skip to content

Commit 06be20e

Browse files
authored
STAC-23598: Restoring Stackgraph (#4)
* STAC-23598: Restoring Stackgraph * STAC-23598: Introducing Dependency Injection to test cli commands * STAC-23598: Rename config.stackgraph.splitArchiveSize to config.stackgraph.multipartArchive * STAC-23598: Adding check-and-finalize command to stackgraph
1 parent 6123f01 commit 06be20e

60 files changed

Lines changed: 7454 additions & 811 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ARCHITECTURE.md

Lines changed: 404 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 73 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ A command-line tool for managing backups and restores for SUSE Observability pla
66

77
This CLI tool replaces the legacy Bash-based backup/restore scripts with a single Go binary that can be run from an operator host. It uses Kubernetes port-forwarding to connect to services and automatically discovers configuration from ConfigMaps and Secrets.
88

9-
**Current Support:** Elasticsearch snapshots and restores
10-
**Planned:** VictoriaMetrics, ClickHouse, StackGraph, Configuration backups
9+
**Current Support:**
10+
- Elasticsearch snapshots and restores
11+
- Stackgraph backups and restores
12+
13+
**Planned:** VictoriaMetrics, ClickHouse, Configuration backups
1114

1215
## Installation
1316

@@ -75,17 +78,45 @@ sts-backup elasticsearch list-snapshots --namespace <namespace>
7578

7679
#### restore-snapshot
7780

78-
Restore Elasticsearch snapshot.
81+
Restore Elasticsearch snapshot. Automatically scales down affected deployments before restore and scales them back up afterward.
7982

8083
```bash
8184
sts-backup elasticsearch restore-snapshot --namespace <namespace> --snapshot-name <name> [flags]
8285
```
8386

8487
**Flags:**
85-
- `--snapshot-name` - Name of snapshot to restore (required)
86-
- `--drop-all-indices` - Delete all existing indices before restore
88+
- `--snapshot-name, -s` - Name of snapshot to restore (required)
89+
- `--drop-all-indices, -r` - Delete all existing STS indices before restore
8790
- `--yes` - Skip confirmation prompt
8891

92+
### stackgraph
93+
94+
Manage Stackgraph backups and restores.
95+
96+
#### list
97+
98+
List available Stackgraph backups from S3/Minio.
99+
100+
```bash
101+
sts-backup stackgraph list --namespace <namespace>
102+
```
103+
104+
#### restore
105+
106+
Restore Stackgraph from a backup archive. Automatically scales down affected deployments before restore and scales them back up afterward.
107+
108+
```bash
109+
sts-backup stackgraph restore --namespace <namespace> [--archive <name> | --latest] [flags]
110+
```
111+
112+
**Flags:**
113+
- `--archive` - Specific archive name to restore (e.g., sts-backup-20210216-0300.graph)
114+
- `--latest` - Restore from the most recent backup
115+
- `--force` - Force delete existing data during restore
116+
- `--background` - Run restore job in background without waiting for completion
117+
118+
**Note**: Either `--archive` or `--latest` must be specified (mutually exclusive).
119+
89120
## Configuration
90121

91122
The CLI uses configuration from Kubernetes ConfigMaps and Secrets with the following precedence:
@@ -149,29 +180,51 @@ kubectl create secret generic suse-observability-backup-config \
149180
-n <namespace>
150181
```
151182

152-
See [internal/config/testdata/validConfigMapConfig.yaml](internal/config/testdata/validConfigMapConfig.yaml) for a complete example.
183+
See [internal/foundation/config/testdata/validConfigMapConfig.yaml](internal/foundation/config/testdata/validConfigMapConfig.yaml) for a complete example.
153184

154185
## Project Structure
155186

156187
```
157188
.
158-
├── cmd/ # CLI commands
159-
│ ├── root.go # Root command and flag definitions
189+
├── cmd/ # CLI commands (Layer 4)
190+
│ ├── root.go # Root command and global flags
160191
│ ├── version/ # Version command
161-
│ └── elasticsearch/ # Elasticsearch subcommands
162-
│ ├── configure.go # Configure snapshot repository
163-
│ ├── list-indices.go # List indices
164-
│ ├── list-snapshots.go # List snapshots
165-
│ └── restore-snapshot.go # Restore snapshot
166-
├── internal/ # Internal packages
167-
│ ├── config/ # Configuration loading and validation
168-
│ ├── elasticsearch/ # Elasticsearch client
169-
│ ├── k8s/ # Kubernetes client utilities
170-
│ ├── logger/ # Structured logging
171-
│ └── output/ # Output formatting (table, JSON)
172-
└── main.go # Entry point
192+
│ ├── elasticsearch/ # Elasticsearch subcommands
193+
│ │ ├── configure.go # Configure snapshot repository
194+
│ │ ├── list-indices.go # List indices
195+
│ │ ├── list-snapshots.go # List snapshots
196+
│ │ └── restore-snapshot.go # Restore snapshot
197+
│ └── stackgraph/ # Stackgraph subcommands
198+
│ ├── list.go # List backups
199+
│ └── restore.go # Restore backup
200+
├── internal/ # Internal packages (Layers 0-3)
201+
│ ├── foundation/ # Layer 0: Core utilities
202+
│ │ ├── config/ # Configuration management
203+
│ │ ├── logger/ # Structured logging
204+
│ │ └── output/ # Output formatting
205+
│ ├── clients/ # Layer 1: Service clients
206+
│ │ ├── k8s/ # Kubernetes client
207+
│ │ ├── elasticsearch/ # Elasticsearch client
208+
│ │ └── s3/ # S3/Minio client
209+
│ ├── orchestration/ # Layer 2: Workflows
210+
│ │ ├── portforward/ # Port-forwarding lifecycle
211+
│ │ └── scale/ # Deployment scaling
212+
│ ├── app/ # Layer 3: Dependency container
213+
│ │ └── app.go # Application context and DI
214+
│ └── scripts/ # Embedded bash scripts
215+
├── main.go # Entry point
216+
└── ARCHITECTURE.md # Detailed architecture documentation
173217
```
174218

219+
### Key Architectural Features
220+
221+
- **Layered Architecture**: Clear separation between commands (Layer 4), dependency injection (Layer 3), workflows (Layer 2), clients (Layer 1), and utilities (Layer 0)
222+
- **Dependency Injection**: Centralized dependency creation via `internal/app/` eliminates boilerplate from commands
223+
- **Testability**: All layers use interfaces for external dependencies, enabling comprehensive unit testing
224+
- **Clean Commands**: Commands are thin (50-100 lines) and focused on business logic
225+
226+
See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed information about the layered architecture and design patterns.
227+
175228
## CI/CD
176229

177230
This project uses GitHub Actions and GoReleaser for automated releases:

cmd/elasticsearch/configure.go

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,70 +5,52 @@ import (
55
"os"
66

77
"github.com/spf13/cobra"
8-
"github.com/stackvista/stackstate-backup-cli/cmd/portforward"
9-
"github.com/stackvista/stackstate-backup-cli/internal/config"
10-
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
11-
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
12-
"github.com/stackvista/stackstate-backup-cli/internal/logger"
8+
"github.com/stackvista/stackstate-backup-cli/internal/app"
9+
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
10+
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/portforward"
1311
)
1412

15-
func configureCmd(cliCtx *config.Context) *cobra.Command {
13+
func configureCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
1614
return &cobra.Command{
1715
Use: "configure",
1816
Short: "Configure Elasticsearch snapshot repository and SLM policy",
1917
Long: `Configure Elasticsearch snapshot repository and Snapshot Lifecycle Management (SLM) policy for automated backups.`,
2018
Run: func(_ *cobra.Command, _ []string) {
21-
if err := runConfigure(cliCtx); err != nil {
19+
appCtx, err := app.NewContext(globalFlags)
20+
if err != nil {
21+
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
22+
os.Exit(1)
23+
}
24+
if err := runConfigure(appCtx); err != nil {
2225
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
2326
os.Exit(1)
2427
}
2528
},
2629
}
2730
}
2831

29-
func runConfigure(cliCtx *config.Context) error {
30-
// Create logger
31-
log := logger.New(cliCtx.Config.Quiet, cliCtx.Config.Debug)
32-
33-
// Create Kubernetes client
34-
k8sClient, err := k8s.NewClient(cliCtx.Config.Kubeconfig, cliCtx.Config.Debug)
35-
if err != nil {
36-
return fmt.Errorf("failed to create Kubernetes client: %w", err)
37-
}
38-
39-
// Load configuration
40-
cfg, err := config.LoadConfig(k8sClient.Clientset(), cliCtx.Config.Namespace, cliCtx.Config.ConfigMapName, cliCtx.Config.SecretName)
41-
if err != nil {
42-
return fmt.Errorf("failed to load configuration: %w", err)
43-
}
44-
32+
func runConfigure(appCtx *app.Context) error {
4533
// Validate required configuration
46-
if cfg.Elasticsearch.SnapshotRepository.AccessKey == "" || cfg.Elasticsearch.SnapshotRepository.SecretKey == "" {
34+
if appCtx.Config.Elasticsearch.SnapshotRepository.AccessKey == "" || appCtx.Config.Elasticsearch.SnapshotRepository.SecretKey == "" {
4735
return fmt.Errorf("accessKey and secretKey are required in the secret configuration")
4836
}
4937

5038
// Setup port-forward to Elasticsearch
51-
serviceName := cfg.Elasticsearch.Service.Name
52-
localPort := cfg.Elasticsearch.Service.LocalPortForwardPort
53-
remotePort := cfg.Elasticsearch.Service.Port
39+
serviceName := appCtx.Config.Elasticsearch.Service.Name
40+
localPort := appCtx.Config.Elasticsearch.Service.LocalPortForwardPort
41+
remotePort := appCtx.Config.Elasticsearch.Service.Port
5442

55-
pf, err := portforward.SetupPortForward(k8sClient, cliCtx.Config.Namespace, serviceName, localPort, remotePort, log)
43+
pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, localPort, remotePort, appCtx.Logger)
5644
if err != nil {
5745
return err
5846
}
5947
defer close(pf.StopChan)
6048

61-
// Create Elasticsearch client
62-
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", pf.LocalPort))
63-
if err != nil {
64-
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
65-
}
66-
6749
// Configure snapshot repository
68-
repo := cfg.Elasticsearch.SnapshotRepository
69-
log.Infof("Configuring snapshot repository '%s' (bucket: %s)...", repo.Name, repo.Bucket)
50+
repo := appCtx.Config.Elasticsearch.SnapshotRepository
51+
appCtx.Logger.Infof("Configuring snapshot repository '%s' (bucket: %s)...", repo.Name, repo.Bucket)
7052

71-
err = esClient.ConfigureSnapshotRepository(
53+
err = appCtx.ESClient.ConfigureSnapshotRepository(
7254
repo.Name,
7355
repo.Bucket,
7456
repo.Endpoint,
@@ -80,13 +62,13 @@ func runConfigure(cliCtx *config.Context) error {
8062
return fmt.Errorf("failed to configure snapshot repository: %w", err)
8163
}
8264

83-
log.Successf("Snapshot repository configured successfully")
65+
appCtx.Logger.Successf("Snapshot repository configured successfully")
8466

8567
// Configure SLM policy
86-
slm := cfg.Elasticsearch.SLM
87-
log.Infof("Configuring SLM policy '%s'...", slm.Name)
68+
slm := appCtx.Config.Elasticsearch.SLM
69+
appCtx.Logger.Infof("Configuring SLM policy '%s'...", slm.Name)
8870

89-
err = esClient.ConfigureSLMPolicy(
71+
err = appCtx.ESClient.ConfigureSLMPolicy(
9072
slm.Name,
9173
slm.Schedule,
9274
slm.SnapshotTemplateName,
@@ -100,9 +82,9 @@ func runConfigure(cliCtx *config.Context) error {
10082
return fmt.Errorf("failed to configure SLM policy: %w", err)
10183
}
10284

103-
log.Successf("SLM policy configured successfully")
104-
log.Println()
105-
log.Successf("Configuration completed successfully")
85+
appCtx.Logger.Successf("SLM policy configured successfully")
86+
appCtx.Logger.Println()
87+
appCtx.Logger.Successf("Configuration completed successfully")
10688

10789
return nil
10890
}

cmd/elasticsearch/configure_test.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"fmt"
66
"testing"
77

8-
"github.com/stackvista/stackstate-backup-cli/internal/config"
9-
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
8+
"github.com/stackvista/stackstate-backup-cli/internal/clients/elasticsearch"
9+
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
1010
"github.com/stretchr/testify/assert"
1111
"github.com/stretchr/testify/require"
1212
corev1 "k8s.io/api/core/v1"
@@ -92,12 +92,12 @@ func (m *mockESClientForConfigure) RolloverDatastream(_ string) error {
9292

9393
// TestConfigureCmd_Unit tests the command structure
9494
func TestConfigureCmd_Unit(t *testing.T) {
95-
cliCtx := config.NewContext()
96-
cliCtx.Config.Namespace = testNamespace
97-
cliCtx.Config.ConfigMapName = testConfigMapName
98-
cliCtx.Config.SecretName = testSecretName
95+
flags := config.NewCLIGlobalFlags()
96+
flags.Namespace = testNamespace
97+
flags.ConfigMapName = testConfigMapName
98+
flags.SecretName = testSecretName
9999

100-
cmd := configureCmd(cliCtx)
100+
cmd := configureCmd(flags)
101101

102102
// Test command metadata
103103
assert.Equal(t, "configure", cmd.Use)
@@ -152,7 +152,7 @@ elasticsearch:
152152
retentionExpireAfter: 30d
153153
retentionMinCount: 5
154154
retentionMaxCount: 50
155-
`,
155+
` + minimalMinioStackgraphConfig,
156156
secretData: "",
157157
expectError: false,
158158
},
@@ -187,20 +187,23 @@ elasticsearch:
187187
retentionExpireAfter: 30d
188188
retentionMinCount: 5
189189
retentionMaxCount: 50
190-
`,
190+
` + minimalMinioStackgraphConfig,
191191
secretData: `
192192
elasticsearch:
193193
snapshotRepository:
194194
accessKey: secret-key
195195
secretKey: secret-value
196+
minio:
197+
accessKey: secret-minio-key
198+
secretKey: secret-minio-value
196199
`,
197200
expectError: false,
198201
},
199202
}
200203

201204
for _, tt := range tests {
202205
t.Run(tt.name, func(t *testing.T) {
203-
fakeClient := fake.NewSimpleClientset()
206+
fakeClient := fake.NewClientset()
204207

205208
// Create ConfigMap
206209
cm := &corev1.ConfigMap{

cmd/elasticsearch/elasticsearch.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ package elasticsearch
22

33
import (
44
"github.com/spf13/cobra"
5-
"github.com/stackvista/stackstate-backup-cli/internal/config"
5+
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
66
)
77

8-
func Cmd(cliCtx *config.Context) *cobra.Command {
8+
func Cmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
99
cmd := &cobra.Command{
1010
Use: "elasticsearch",
1111
Short: "Elasticsearch backup and restore operations",
1212
}
1313

14-
cmd.AddCommand(listSnapshotsCmd(cliCtx))
15-
cmd.AddCommand(listIndicesCmd(cliCtx))
16-
cmd.AddCommand(restoreCmd(cliCtx))
17-
cmd.AddCommand(configureCmd(cliCtx))
14+
cmd.AddCommand(listSnapshotsCmd(globalFlags))
15+
cmd.AddCommand(listIndicesCmd(globalFlags))
16+
cmd.AddCommand(restoreCmd(globalFlags))
17+
cmd.AddCommand(configureCmd(globalFlags))
1818

1919
return cmd
2020
}

0 commit comments

Comments
 (0)