Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
147 changes: 120 additions & 27 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ The codebase follows several key principles:

```
stackstate-backup-cli/
β”œβ”€β”€ cmd/ # Command-line interface (Layer 3)
β”œβ”€β”€ cmd/ # Command-line interface (Layer 4)
β”‚ β”œβ”€β”€ root.go # Root command and global flags
β”‚ β”œβ”€β”€ version/ # Version information command
β”‚ β”œβ”€β”€ elasticsearch/ # Elasticsearch backup/restore commands
β”‚ └── stackgraph/ # Stackgraph backup/restore commands
β”‚
β”œβ”€β”€ internal/ # Internal packages (Layers 0-2)
β”œβ”€β”€ internal/ # Internal packages (Layers 0-3)
β”‚ β”œβ”€β”€ foundation/ # Layer 0: Core utilities
β”‚ β”‚ β”œβ”€β”€ config/ # Configuration management
β”‚ β”‚ β”œβ”€β”€ logger/ # Structured logging
Expand All @@ -37,6 +37,9 @@ stackstate-backup-cli/
β”‚ β”‚ β”œβ”€β”€ portforward/ # Port-forwarding orchestration
β”‚ β”‚ └── scale/ # Deployment scaling workflows
β”‚ β”‚
β”‚ β”œβ”€β”€ app/ # Layer 3: Dependency Container
β”‚ β”‚ └── app.go # Application context and dependency injection
β”‚ β”‚
β”‚ └── scripts/ # Embedded bash scripts
β”‚
β”œβ”€β”€ main.go # Application entry point
Expand All @@ -46,24 +49,61 @@ stackstate-backup-cli/

## Architectural Layers

### Layer 3: Commands (`cmd/`)
### Layer 4: Commands (`cmd/`)

**Purpose**: User-facing CLI commands and application entry points

**Characteristics**:
- Implements the Cobra command structure
- Handles user input validation and flag parsing
- Orchestrates calls to lower layers
- Delegates to orchestration and client layers via app context
- Minimal business logic (thin command layer)
- Formats output for end users

**Key Packages**:
- `cmd/elasticsearch/`: Elasticsearch snapshot/restore commands (configure, list-snapshots, list-indices, restore-snapshot)
- `cmd/stackgraph/`: Stackgraph backup/restore commands (list, restore)
- `cmd/version/`: Version information

**Dependency Rules**:
- βœ… Can import: `internal/app/*` (preferred), all other `internal/` packages
- ❌ Should not: Create clients directly, contain business logic

### Layer 3: Dependency Container (`internal/app/`)

**Purpose**: Centralized dependency initialization and injection

**Characteristics**:
- Creates and wires all application dependencies
- Provides single entry point for dependency creation
- Eliminates boilerplate from commands
- Improves testability through centralized mocking

**Key Components**:
- `Context`: Struct holding all dependencies (K8s client, S3 client, ES client, config, logger, formatter)
- `NewContext()`: Factory function creating production dependencies from global flags

**Usage Pattern**:
```go
// In command files
appCtx, err := app.NewContext(globalFlags)
if err != nil {
return err
}

// All dependencies available via appCtx
appCtx.K8sClient
appCtx.S3Client
appCtx.ESClient
appCtx.Config
appCtx.Logger
appCtx.Formatter
```

**Dependency Rules**:
- βœ… Can import: All `internal/` packages
- ❌ Should not: Contain business logic or direct service calls
- βœ… Used by: `cmd/` layer only
- ❌ Should not: Contain business logic or orchestration

### Layer 2: Orchestration (`internal/orchestration/`)

Expand Down Expand Up @@ -130,31 +170,63 @@ stackstate-backup-cli/
└─> cmd/elasticsearch/restore-snapshot.go
β”‚
2. Parse flags and validate input
└─> Cobra command receives global flags
β”‚
3. Load configuration
└─> internal/foundation/config/
β”‚
4. Create clients
└─> internal/clients/k8s/
└─> internal/clients/elasticsearch/
3. Create application context with dependencies
└─> app.NewContext(globalFlags)
β”œβ”€> internal/clients/k8s/ (K8s client)
β”œβ”€> internal/foundation/config/ (Load from ConfigMap/Secret)
β”œβ”€> internal/clients/s3/ (S3/Minio client)
β”œβ”€> internal/clients/elasticsearch/ (ES client)
β”œβ”€> internal/foundation/logger/ (Logger)
└─> internal/foundation/output/ (Formatter)
β”‚
5. Execute orchestration workflow
└─> internal/orchestration/scale/
└─> Scale down deployments
└─> internal/orchestration/portforward/
└─> Setup port-forward to Elasticsearch
└─> internal/clients/elasticsearch/
└─> Perform snapshot restore
└─> internal/orchestration/scale/
└─> Scale up deployments
4. Execute business logic with injected dependencies
└─> runRestore(appCtx)
β”œβ”€> internal/orchestration/scale/ (Scale down)
β”œβ”€> internal/orchestration/portforward/ (Port-forward)
β”œβ”€> internal/clients/elasticsearch/ (Restore snapshot)
└─> internal/orchestration/scale/ (Scale up)
β”‚
6. Format and display results
└─> internal/foundation/output/
5. Format and display results
└─> appCtx.Formatter.PrintTable() or PrintJSON()
```

## Key Design Patterns

### 1. Configuration Precedence
### 1. Dependency Injection Pattern

All dependencies are created once and injected via `app.Context`:

```go
// Before (repeated in every command)
func runList(globalFlags *config.CLIGlobalFlags) error {
k8sClient, _ := k8s.NewClient(...)
cfg, _ := config.LoadConfig(...)
s3Client, _ := s3.NewClient(...)
log := logger.New(...)
formatter := output.NewFormatter(...)
// ... use dependencies
}

// After (centralized creation)
func runList(appCtx *app.Context) error {
// All dependencies available immediately
appCtx.K8sClient
appCtx.Config
appCtx.S3Client
appCtx.Logger
appCtx.Formatter
}
```

**Benefits**:
- Eliminates boilerplate from commands (30-50 lines per command)
- Centralized dependency creation makes testing easier
- Single source of truth for dependency wiring
- Commands are thinner and more focused on business logic

### 2. Configuration Precedence

Configuration is loaded with the following precedence (highest to lowest):

Expand All @@ -166,7 +238,7 @@ Configuration is loaded with the following precedence (highest to lowest):

Implementation: `internal/foundation/config/config.go`

### 2. Client Factory Pattern
### 3. Client Factory Pattern

Clients are created with a consistent factory pattern:

Expand All @@ -178,7 +250,7 @@ func NewClient(endpoint string) (*Client, error) {
}
```

### 3. Port-Forward Lifecycle
### 4. Port-Forward Lifecycle

Services running in Kubernetes are accessed via automatic port-forwarding:

Expand All @@ -188,7 +260,7 @@ pf, err := SetupPortForward(k8sClient, namespace, service, localPort, remotePort
defer close(pf.StopChan) // Automatic cleanup
```

### 4. Scale Down/Up Pattern
### 5. Scale Down/Up Pattern

Deployments are scaled down before restore operations and scaled up afterward:

Expand All @@ -198,7 +270,7 @@ scaledDeployments, _ := scale.ScaleDown(k8sClient, namespace, selector, log)
defer scale.ScaleUp(k8sClient, namespace, scaledDeployments, log)
```

### 5. Structured Logging
### 6. Structured Logging

All operations use structured logging with consistent levels:

Expand Down Expand Up @@ -292,6 +364,27 @@ endpoint := "http://localhost:9200"

**Fix**: Use configuration management: `config.Elasticsearch.Service.Name`

### ❌ Don't: Create Clients Directly in Commands

```go
// BAD: cmd/elasticsearch/list-snapshots.go
func runListSnapshots(globalFlags *config.CLIGlobalFlags) error {
k8sClient, _ := k8s.NewClient(globalFlags.Kubeconfig, globalFlags.Debug)
esClient, _ := elasticsearch.NewClient("http://localhost:9200")
// ... use clients
}
```

**Fix**: Use `app.Context` for dependency injection:
```go
// GOOD
func runListSnapshots(appCtx *app.Context) error {
// Dependencies already created
appCtx.K8sClient
appCtx.ESClient
}
```

## Automated Enforcement

Verify architectural rules with these commands:
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ See [internal/foundation/config/testdata/validConfigMapConfig.yaml](internal/fou

```
.
β”œβ”€β”€ cmd/ # CLI commands
β”œβ”€β”€ cmd/ # CLI commands (Layer 4)
β”‚ β”œβ”€β”€ root.go # Root command and global flags
β”‚ β”œβ”€β”€ version/ # Version command
β”‚ β”œβ”€β”€ elasticsearch/ # Elasticsearch subcommands
Expand All @@ -197,7 +197,7 @@ See [internal/foundation/config/testdata/validConfigMapConfig.yaml](internal/fou
β”‚ └── stackgraph/ # Stackgraph subcommands
β”‚ β”œβ”€β”€ list.go # List backups
β”‚ └── restore.go # Restore backup
β”œβ”€β”€ internal/ # Internal packages (layered architecture)
β”œβ”€β”€ internal/ # Internal packages (Layers 0-3)
β”‚ β”œβ”€β”€ foundation/ # Layer 0: Core utilities
β”‚ β”‚ β”œβ”€β”€ config/ # Configuration management
β”‚ β”‚ β”œβ”€β”€ logger/ # Structured logging
Expand All @@ -209,11 +209,20 @@ See [internal/foundation/config/testdata/validConfigMapConfig.yaml](internal/fou
β”‚ β”œβ”€β”€ 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
Expand Down
66 changes: 24 additions & 42 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/internal/clients/elasticsearch"
"github.com/stackvista/stackstate-backup-cli/internal/clients/k8s"
"github.com/stackvista/stackstate-backup-cli/internal/app"
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
"github.com/stackvista/stackstate-backup-cli/internal/foundation/logger"
"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
}
Loading