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
20 changes: 20 additions & 0 deletions cmd/elasticsearch/list_snapshots_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,26 @@ victoriaMetrics:
requests:
cpu: "500m"
memory: "1Gi"
settings:
bucket: sts-settings-backup
s3Prefix: ""
restore:
scaleDownLabelSelector: "app=settings"
loggingConfigConfigMap: logging-config
baseUrl: "http://server:7070"
receiverBaseUrl: "http://receiver:7077"
platformVersion: "5.2.0"
zookeeperQuorum: "zookeeper:2181"
job:
image: settings-backup:latest
waitImage: wait:latest
resources:
limits:
cpu: "1"
memory: "2Gi"
requests:
cpu: "500m"
memory: "1Gi"
`

// mockESClient is a simple mock for testing commands
Expand Down
5 changes: 5 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/spf13/cobra"
"github.com/stackvista/stackstate-backup-cli/cmd/elasticsearch"
"github.com/stackvista/stackstate-backup-cli/cmd/settings"
"github.com/stackvista/stackstate-backup-cli/cmd/stackgraph"
"github.com/stackvista/stackstate-backup-cli/cmd/version"
"github.com/stackvista/stackstate-backup-cli/cmd/victoriametrics"
Expand Down Expand Up @@ -40,6 +41,10 @@ func init() {
addBackupConfigFlags(stackgraphCmd)
rootCmd.AddCommand(stackgraphCmd)

settingsCmd := settings.Cmd(flags)
addBackupConfigFlags(settingsCmd)
rootCmd.AddCommand(settingsCmd)

victoriaMetricsCmd := victoriametrics.Cmd(flags)
addBackupConfigFlags(victoriaMetricsCmd)
rootCmd.AddCommand(victoriaMetricsCmd)
Expand Down
65 changes: 65 additions & 0 deletions cmd/settings/check_and_finalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package settings

import (
"fmt"
"os"

"github.com/spf13/cobra"
"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/restore"
)

// Check and finalize command flags
var (
checkJobName string
waitForJob bool
)

func checkAndFinalizeCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
cmd := &cobra.Command{
Use: "check-and-finalize",
Short: "Check and finalize a Settings restore job",
Long: `Check the status of a background Settings restore job and clean up resources.

This command is useful when a restore job was started with --background flag or was interrupted (Ctrl+C).
It will check the job status, print logs if it failed, and clean up the job and PVC resources.

Examples:
# Check job status without waiting
sts-backup settings check-and-finalize --job settings-restore-20250128t143000 -n my-namespace

# Wait for job completion and cleanup
sts-backup settings check-and-finalize --job settings-restore-20250128t143000 --wait -n my-namespace`,
Run: func(_ *cobra.Command, _ []string) {
appCtx, err := app.NewContext(globalFlags)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := runCheckAndFinalize(appCtx); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
},
}

cmd.Flags().StringVarP(&checkJobName, "job", "j", "", "Settings restore job name (required)")
cmd.Flags().BoolVarP(&waitForJob, "wait", "w", false, "Wait for job to complete before cleanup")
_ = cmd.MarkFlagRequired("job")

return cmd
}

func runCheckAndFinalize(appCtx *app.Context) error {
return restore.CheckAndFinalize(restore.CheckAndFinalizeParams{
K8sClient: appCtx.K8sClient,
Namespace: appCtx.Namespace,
JobName: checkJobName,
ServiceName: "settings",
ScaleSelector: appCtx.Config.Settings.Restore.ScaleDownLabelSelector,
CleanupPVC: true,
WaitForJob: waitForJob,
Log: appCtx.Logger,
})
}
97 changes: 97 additions & 0 deletions cmd/settings/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package settings

import (
"context"
"fmt"
"os"
"sort"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/spf13/cobra"
"github.com/stackvista/stackstate-backup-cli/internal/app"
s3client "github.com/stackvista/stackstate-backup-cli/internal/clients/s3"
"github.com/stackvista/stackstate-backup-cli/internal/foundation/config"
"github.com/stackvista/stackstate-backup-cli/internal/foundation/output"
"github.com/stackvista/stackstate-backup-cli/internal/orchestration/portforward"
)

const (
isMultiPartArchive = false
)

func listCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List available Settings backups from S3/Minio",
Run: func(_ *cobra.Command, _ []string) {
appCtx, err := app.NewContext(globalFlags)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if err := runList(appCtx); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
},
}
}

func runList(appCtx *app.Context) error {
// Setup port-forward to Minio
serviceName := appCtx.Config.Minio.Service.Name
localPort := appCtx.Config.Minio.Service.LocalPortForwardPort
remotePort := appCtx.Config.Minio.Service.Port

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

// List objects in bucket
bucket := appCtx.Config.Settings.Bucket
prefix := appCtx.Config.Settings.S3Prefix

appCtx.Logger.Infof("Listing Settings backups in bucket '%s'...", bucket)

input := &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(prefix),
}

result, err := appCtx.S3Client.ListObjectsV2(context.Background(), input)
if err != nil {
return fmt.Errorf("failed to list S3 objects: %w", err)
}

// Filter objects based on whether the archive is split or not
filteredObjects := s3client.FilterBackupObjects(result.Contents, isMultiPartArchive)

// Sort by LastModified time (most recent first)
sort.Slice(filteredObjects, func(i, j int) bool {
return filteredObjects[i].LastModified.After(filteredObjects[j].LastModified)
})

if len(filteredObjects) == 0 {
appCtx.Formatter.PrintMessage("No backups found")
return nil
}

table := output.Table{
Headers: []string{"NAME", "LAST MODIFIED", "SIZE"},
Rows: make([][]string, 0, len(filteredObjects)),
}

for _, obj := range filteredObjects {
row := []string{
obj.Key,
obj.LastModified.Format("2006-01-02 15:04:05 MST"),
output.FormatBytes(obj.Size),
}
table.Rows = append(table.Rows, row)
}

return appCtx.Formatter.PrintTable(table)
}
Loading