Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 404 additions & 0 deletions ARCHITECTURE.md

Large diffs are not rendered by default.

93 changes: 73 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ A command-line tool for managing backups and restores for SUSE Observability pla

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.

**Current Support:** Elasticsearch snapshots and restores
**Planned:** VictoriaMetrics, ClickHouse, StackGraph, Configuration backups
**Current Support:**
- Elasticsearch snapshots and restores
- Stackgraph backups and restores

**Planned:** VictoriaMetrics, ClickHouse, Configuration backups

## Installation

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

#### restore-snapshot

Restore Elasticsearch snapshot.
Restore Elasticsearch snapshot. Automatically scales down affected deployments before restore and scales them back up afterward.

```bash
sts-backup elasticsearch restore-snapshot --namespace <namespace> --snapshot-name <name> [flags]
```

**Flags:**
- `--snapshot-name` - Name of snapshot to restore (required)
- `--drop-all-indices` - Delete all existing indices before restore
- `--snapshot-name, -s` - Name of snapshot to restore (required)
- `--drop-all-indices, -r` - Delete all existing STS indices before restore
- `--yes` - Skip confirmation prompt

### stackgraph

Manage Stackgraph backups and restores.

#### list

List available Stackgraph backups from S3/Minio.

```bash
sts-backup stackgraph list --namespace <namespace>
```

#### restore

Restore Stackgraph from a backup archive. Automatically scales down affected deployments before restore and scales them back up afterward.

```bash
sts-backup stackgraph restore --namespace <namespace> [--archive <name> | --latest] [flags]
```

**Flags:**
- `--archive` - Specific archive name to restore (e.g., sts-backup-20210216-0300.graph)
- `--latest` - Restore from the most recent backup
- `--force` - Force delete existing data during restore
- `--background` - Run restore job in background without waiting for completion

**Note**: Either `--archive` or `--latest` must be specified (mutually exclusive).

## Configuration

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

See [internal/config/testdata/validConfigMapConfig.yaml](internal/config/testdata/validConfigMapConfig.yaml) for a complete example.
See [internal/foundation/config/testdata/validConfigMapConfig.yaml](internal/foundation/config/testdata/validConfigMapConfig.yaml) for a complete example.

## Project Structure

```
.
β”œβ”€β”€ cmd/ # CLI commands
β”‚ β”œβ”€β”€ root.go # Root command and flag definitions
β”œβ”€β”€ cmd/ # CLI commands (Layer 4)
β”‚ β”œβ”€β”€ root.go # Root command and global flags
β”‚ β”œβ”€β”€ version/ # Version command
β”‚ └── elasticsearch/ # Elasticsearch subcommands
β”‚ β”œβ”€β”€ configure.go # Configure snapshot repository
β”‚ β”œβ”€β”€ list-indices.go # List indices
β”‚ β”œβ”€β”€ list-snapshots.go # List snapshots
β”‚ └── restore-snapshot.go # Restore snapshot
β”œβ”€β”€ internal/ # Internal packages
β”‚ β”œβ”€β”€ config/ # Configuration loading and validation
β”‚ β”œβ”€β”€ elasticsearch/ # Elasticsearch client
β”‚ β”œβ”€β”€ k8s/ # Kubernetes client utilities
β”‚ β”œβ”€β”€ logger/ # Structured logging
β”‚ └── output/ # Output formatting (table, JSON)
└── main.go # Entry point
β”‚ β”œβ”€β”€ elasticsearch/ # Elasticsearch subcommands
β”‚ β”‚ β”œβ”€β”€ configure.go # Configure snapshot repository
β”‚ β”‚ β”œβ”€β”€ list-indices.go # List indices
β”‚ β”‚ β”œβ”€β”€ list-snapshots.go # List snapshots
β”‚ β”‚ └── restore-snapshot.go # Restore snapshot
β”‚ └── stackgraph/ # Stackgraph subcommands
β”‚ β”œβ”€β”€ list.go # List backups
β”‚ └── restore.go # Restore backup
β”œβ”€β”€ internal/ # Internal packages (Layers 0-3)
β”‚ β”œβ”€β”€ foundation/ # Layer 0: Core utilities
β”‚ β”‚ β”œβ”€β”€ config/ # Configuration management
β”‚ β”‚ β”œβ”€β”€ logger/ # Structured logging
β”‚ β”‚ └── output/ # Output formatting
β”‚ β”œβ”€β”€ clients/ # Layer 1: Service clients
β”‚ β”‚ β”œβ”€β”€ k8s/ # Kubernetes client
β”‚ β”‚ β”œβ”€β”€ elasticsearch/ # Elasticsearch client
β”‚ β”‚ └── s3/ # S3/Minio client
β”‚ β”œβ”€β”€ orchestration/ # Layer 2: Workflows
β”‚ β”‚ β”œβ”€β”€ portforward/ # Port-forwarding lifecycle
β”‚ β”‚ └── scale/ # Deployment scaling
β”‚ β”œβ”€β”€ app/ # Layer 3: Dependency container
β”‚ β”‚ └── app.go # Application context and DI
β”‚ └── scripts/ # Embedded bash scripts
β”œβ”€β”€ main.go # Entry point
└── ARCHITECTURE.md # Detailed architecture documentation
```

### Key Architectural Features

- **Layered Architecture**: Clear separation between commands (Layer 4), dependency injection (Layer 3), workflows (Layer 2), clients (Layer 1), and utilities (Layer 0)
- **Dependency Injection**: Centralized dependency creation via `internal/app/` eliminates boilerplate from commands
- **Testability**: All layers use interfaces for external dependencies, enabling comprehensive unit testing
- **Clean Commands**: Commands are thin (50-100 lines) and focused on business logic

See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed information about the layered architecture and design patterns.

## CI/CD

This project uses GitHub Actions and GoReleaser for automated releases:
Expand Down
70 changes: 26 additions & 44 deletions cmd/elasticsearch/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,52 @@ import (
"os"

"github.com/spf13/cobra"
"github.com/stackvista/stackstate-backup-cli/cmd/portforward"
"github.com/stackvista/stackstate-backup-cli/internal/config"
"github.com/stackvista/stackstate-backup-cli/internal/elasticsearch"
"github.com/stackvista/stackstate-backup-cli/internal/k8s"
"github.com/stackvista/stackstate-backup-cli/internal/logger"
"github.com/stackvista/stackstate-backup-cli/internal/app"
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/portforward"
)

func configureCmd(cliCtx *config.Context) *cobra.Command {
func configureCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
return &cobra.Command{
Use: "configure",
Short: "Configure Elasticsearch snapshot repository and SLM policy",
Long: `Configure Elasticsearch snapshot repository and Snapshot Lifecycle Management (SLM) policy for automated backups.`,
Run: func(_ *cobra.Command, _ []string) {
if err := runConfigure(cliCtx); err != nil {
appCtx, err := app.NewContext(globalFlags)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := runConfigure(appCtx); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
},
}
}

func runConfigure(cliCtx *config.Context) error {
// Create logger
log := logger.New(cliCtx.Config.Quiet, cliCtx.Config.Debug)

// Create Kubernetes client
k8sClient, err := k8s.NewClient(cliCtx.Config.Kubeconfig, cliCtx.Config.Debug)
if err != nil {
return fmt.Errorf("failed to create Kubernetes client: %w", err)
}

// Load configuration
cfg, err := config.LoadConfig(k8sClient.Clientset(), cliCtx.Config.Namespace, cliCtx.Config.ConfigMapName, cliCtx.Config.SecretName)
if err != nil {
return fmt.Errorf("failed to load configuration: %w", err)
}

func runConfigure(appCtx *app.Context) error {
// Validate required configuration
if cfg.Elasticsearch.SnapshotRepository.AccessKey == "" || cfg.Elasticsearch.SnapshotRepository.SecretKey == "" {
if appCtx.Config.Elasticsearch.SnapshotRepository.AccessKey == "" || appCtx.Config.Elasticsearch.SnapshotRepository.SecretKey == "" {
return fmt.Errorf("accessKey and secretKey are required in the secret configuration")
}

// Setup port-forward to Elasticsearch
serviceName := cfg.Elasticsearch.Service.Name
localPort := cfg.Elasticsearch.Service.LocalPortForwardPort
remotePort := cfg.Elasticsearch.Service.Port
serviceName := appCtx.Config.Elasticsearch.Service.Name
localPort := appCtx.Config.Elasticsearch.Service.LocalPortForwardPort
remotePort := appCtx.Config.Elasticsearch.Service.Port

pf, err := portforward.SetupPortForward(k8sClient, cliCtx.Config.Namespace, serviceName, localPort, remotePort, log)
pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, localPort, remotePort, appCtx.Logger)
if err != nil {
return err
}
defer close(pf.StopChan)

// Create Elasticsearch client
esClient, err := elasticsearch.NewClient(fmt.Sprintf("http://localhost:%d", pf.LocalPort))
if err != nil {
return fmt.Errorf("failed to create Elasticsearch client: %w", err)
}

// Configure snapshot repository
repo := cfg.Elasticsearch.SnapshotRepository
log.Infof("Configuring snapshot repository '%s' (bucket: %s)...", repo.Name, repo.Bucket)
repo := appCtx.Config.Elasticsearch.SnapshotRepository
appCtx.Logger.Infof("Configuring snapshot repository '%s' (bucket: %s)...", repo.Name, repo.Bucket)

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

log.Successf("Snapshot repository configured successfully")
appCtx.Logger.Successf("Snapshot repository configured successfully")

// Configure SLM policy
slm := cfg.Elasticsearch.SLM
log.Infof("Configuring SLM policy '%s'...", slm.Name)
slm := appCtx.Config.Elasticsearch.SLM
appCtx.Logger.Infof("Configuring SLM policy '%s'...", slm.Name)

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

log.Successf("SLM policy configured successfully")
log.Println()
log.Successf("Configuration completed successfully")
appCtx.Logger.Successf("SLM policy configured successfully")
appCtx.Logger.Println()
appCtx.Logger.Successf("Configuration completed successfully")

return nil
}
23 changes: 13 additions & 10 deletions cmd/elasticsearch/configure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"fmt"
"testing"

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

// TestConfigureCmd_Unit tests the command structure
func TestConfigureCmd_Unit(t *testing.T) {
cliCtx := config.NewContext()
cliCtx.Config.Namespace = testNamespace
cliCtx.Config.ConfigMapName = testConfigMapName
cliCtx.Config.SecretName = testSecretName
flags := config.NewCLIGlobalFlags()
flags.Namespace = testNamespace
flags.ConfigMapName = testConfigMapName
flags.SecretName = testSecretName

cmd := configureCmd(cliCtx)
cmd := configureCmd(flags)

// Test command metadata
assert.Equal(t, "configure", cmd.Use)
Expand Down Expand Up @@ -152,7 +152,7 @@ elasticsearch:
retentionExpireAfter: 30d
retentionMinCount: 5
retentionMaxCount: 50
`,
` + minimalMinioStackgraphConfig,
secretData: "",
expectError: false,
},
Expand Down Expand Up @@ -187,20 +187,23 @@ elasticsearch:
retentionExpireAfter: 30d
retentionMinCount: 5
retentionMaxCount: 50
`,
` + minimalMinioStackgraphConfig,
secretData: `
elasticsearch:
snapshotRepository:
accessKey: secret-key
secretKey: secret-value
minio:
accessKey: secret-minio-key
secretKey: secret-minio-value
`,
expectError: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := fake.NewSimpleClientset()
fakeClient := fake.NewClientset()

// Create ConfigMap
cm := &corev1.ConfigMap{
Expand Down
12 changes: 6 additions & 6 deletions cmd/elasticsearch/elasticsearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ package elasticsearch

import (
"github.com/spf13/cobra"
"github.com/stackvista/stackstate-backup-cli/internal/config"
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
)

func Cmd(cliCtx *config.Context) *cobra.Command {
func Cmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "elasticsearch",
Short: "Elasticsearch backup and restore operations",
}

cmd.AddCommand(listSnapshotsCmd(cliCtx))
cmd.AddCommand(listIndicesCmd(cliCtx))
cmd.AddCommand(restoreCmd(cliCtx))
cmd.AddCommand(configureCmd(cliCtx))
cmd.AddCommand(listSnapshotsCmd(globalFlags))
cmd.AddCommand(listIndicesCmd(globalFlags))
cmd.AddCommand(restoreCmd(globalFlags))
cmd.AddCommand(configureCmd(globalFlags))

return cmd
}
Loading