Skip to content
Open
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
15 changes: 10 additions & 5 deletions cmd/cmdutils/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@ import (
)

const (
MinioIsRequired bool = true
MinioIsNotRequired bool = false
StorageIsRequired bool = true
StorageIsNotRequired bool = false

// MinioIsRequired is deprecated: use StorageIsRequired instead
MinioIsRequired = StorageIsRequired
// MinioIsNotRequired is deprecated: use StorageIsNotRequired instead
MinioIsNotRequired = StorageIsNotRequired
)

func Run(globalFlags *config.CLIGlobalFlags, runFunc func(ctx *app.Context) error, minioRequired bool) {
func Run(globalFlags *config.CLIGlobalFlags, runFunc func(ctx *app.Context) error, storageRequired bool) {
appCtx, err := app.NewContext(globalFlags)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "❌ Error: %v\n", err)
os.Exit(1)
}
if minioRequired && !appCtx.Config.Minio.Enabled {
appCtx.Logger.Errorf("commands that interact with Minio require SUSE Observability to be deployed with .Values.global.backup.enabled=true")
if storageRequired && !appCtx.Config.StorageEnabled() {
appCtx.Logger.Errorf("commands that interact with S3-compatible storage require SUSE Observability to be deployed with .Values.global.backup.enabled=true")
os.Exit(1)
}
if err := runFunc(appCtx); err != nil {
Expand Down
101 changes: 87 additions & 14 deletions cmd/settings/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@ const (
expectedListJobContainerCount = 1
)

// Shared flag for --from-pvc, used by both list and restore commands
var fromPVC bool

func listCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "list",
Short: "List available Settings backups from S3/Minio",
Run: func(_ *cobra.Command, _ []string) {
cmdutils.Run(globalFlags, runList, cmdutils.MinioIsNotRequired)
},
}

cmd.Flags().BoolVar(&fromPVC, "from-pvc", false, "List backups from legacy PVC instead of S3")

return cmd
}

func runList(appCtx *app.Context) error {
Expand Down Expand Up @@ -68,24 +75,48 @@ func runList(appCtx *app.Context) error {
return appCtx.Formatter.PrintTable(table)
}

// getAllBackups retrieves backups from all sources (S3 and PVC), deduplicates and sorts them by LastModified time (most recent first)
// getAllBackups retrieves backups from all sources, deduplicates and sorts them by LastModified time (most recent first).
// When --from-pvc is set: only lists backups from the legacy PVC (requires settings.restore.pvc to be configured).
// In legacy mode (Minio): combines S3 backups (if Minio enabled) + PVC backups.
// In new mode (Storage): combines S3 backups + local bucket backups (from settings.localBucket).
func getAllBackups(appCtx *app.Context) ([]BackupFileInfo, error) {
var backups []BackupFileInfo
var err error

// Get backups from S3 if enabled
if appCtx.Config.Minio.Enabled {
// When --from-pvc is set, only list from the PVC
if fromPVC {
if appCtx.Config.Settings.Restore.PVC == "" {
return nil, fmt.Errorf("--from-pvc requires settings.restore.pvc to be configured")
}
appCtx.Logger.Infof("Listing backups from legacy PVC '%s'...", appCtx.Config.Settings.Restore.PVC)
pvcBackups, err := getBackupListFromPVC(appCtx)
if err != nil {
return nil, fmt.Errorf("failed to get list of backups from PVC: %v", err)
}
return pvcBackups, nil
}

// Get backups from S3 if storage is enabled
if appCtx.Config.StorageEnabled() {
if backups, err = getBackupListFromS3(appCtx); err != nil {
return nil, fmt.Errorf("failed to get list of backups from Minio: %v", err)
return nil, fmt.Errorf("failed to get list of backups from S3 storage: %v", err)
}
}

// Get backups from PVC
backupsFromPvc, err := getBackupListFromPVC(appCtx)
if err != nil {
return nil, fmt.Errorf("failed to get list of backups from PVC: %v", err)
// Get local backups: from PVC in legacy mode, from localBucket in new mode
var localBackups []BackupFileInfo
if appCtx.Config.IsLegacyMode() {
localBackups, err = getBackupListFromPVC(appCtx)
if err != nil {
return nil, fmt.Errorf("failed to get list of backups from PVC: %v", err)
}
} else {
localBackups, err = getBackupListFromLocalBucket(appCtx)
if err != nil {
return nil, fmt.Errorf("failed to get list of backups from local bucket: %v", err)
}
}
backups = append(backups, backupsFromPvc...)
backups = append(backups, localBackups...)

if len(backups) == 0 {
return []BackupFileInfo{}, nil
Expand Down Expand Up @@ -117,10 +148,11 @@ type BackupFileInfo struct {
}

func getBackupListFromS3(appCtx *app.Context) ([]BackupFileInfo, error) {
// Setup port-forward to Minio
serviceName := appCtx.Config.Minio.Service.Name
localPort := appCtx.Config.Minio.Service.LocalPortForwardPort
remotePort := appCtx.Config.Minio.Service.Port
// Setup port-forward to S3-compatible storage
storageService := appCtx.Config.GetStorageService()
serviceName := storageService.Name
localPort := storageService.LocalPortForwardPort
remotePort := storageService.Port

pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, localPort, remotePort, appCtx.Logger)
if err != nil {
Expand Down Expand Up @@ -159,6 +191,47 @@ func getBackupListFromS3(appCtx *app.Context) ([]BackupFileInfo, error) {
return backups, nil
}

// getBackupListFromLocalBucket lists settings backups from the local S3 bucket (new mode).
// This replaces PVC-based listing when using the new storage configuration.
func getBackupListFromLocalBucket(appCtx *app.Context) ([]BackupFileInfo, error) {
// Setup port-forward to S3-compatible storage
storageService := appCtx.Config.GetStorageService()
serviceName := storageService.Name
localPort := storageService.LocalPortForwardPort
remotePort := storageService.Port

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

bucket := appCtx.Config.Settings.LocalBucket

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

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

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

filteredObjects := s3client.FilterBackupObjects(result.Contents, isMultiPartArchive)

var backups []BackupFileInfo
for _, obj := range filteredObjects {
backups = append(backups, BackupFileInfo{
Filename: obj.Key,
LastModified: obj.LastModified,
Size: obj.Size,
})
}
return backups, nil
}

func getBackupListFromPVC(appCtx *app.Context) ([]BackupFileInfo, error) {
// Setup Kubernetes resources for list job
appCtx.Logger.Println()
Expand Down
41 changes: 31 additions & 10 deletions cmd/settings/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,19 @@ func restoreCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
cmd.Flags().BoolVar(&useLatest, "latest", false, "Restore from the most recent backup")
cmd.Flags().BoolVar(&background, "background", false, "Run restore job in background without waiting for completion")
cmd.Flags().BoolVarP(&skipConfirmation, "yes", "y", false, "Skip confirmation prompt")
cmd.Flags().BoolVar(&fromPVC, "from-pvc", false, "Restore backup from legacy PVC instead of S3")
cmd.MarkFlagsMutuallyExclusive("archive", "latest")
cmd.MarkFlagsOneRequired("archive", "latest")

return cmd
}

func runRestore(appCtx *app.Context) error {
// Validate --from-pvc: PVC must be configured
if fromPVC && appCtx.Config.Settings.Restore.PVC == "" {
return fmt.Errorf("--from-pvc requires settings.restore.pvc to be configured")
}

// Determine which archive to restore
backupFile := archiveName
if useLatest {
Expand Down Expand Up @@ -182,34 +188,45 @@ func createRestoreJob(k8sClient *k8s.Client, namespace, jobName, backupFile stri

// buildEnvVar constructs environment variables for the container spec
func buildEnvVar(extraEnvVar []corev1.EnvVar, config *config.Config) []corev1.EnvVar {
storageService := config.GetStorageService()
commonVar := []corev1.EnvVar{
{Name: "BACKUP_CONFIGURATION_BUCKET_NAME", Value: config.Settings.Bucket},
{Name: "BACKUP_CONFIGURATION_S3_PREFIX", Value: config.Settings.S3Prefix},
{Name: "MINIO_ENDPOINT", Value: fmt.Sprintf("%s:%d", config.Minio.Service.Name, config.Minio.Service.Port)},
{Name: "MINIO_ENDPOINT", Value: fmt.Sprintf("%s:%d", storageService.Name, storageService.Port)},
{Name: "STACKSTATE_BASE_URL", Value: config.Settings.Restore.BaseURL},
{Name: "RECEIVER_BASE_URL", Value: config.Settings.Restore.ReceiverBaseURL},
{Name: "PLATFORM_VERSION", Value: config.Settings.Restore.PlatformVersion},
{Name: "ZOOKEEPER_QUORUM", Value: config.Settings.Restore.ZookeeperQuorum},
{Name: "BACKUP_CONFIGURATION_UPLOAD_REMOTE", Value: strconv.FormatBool(config.Minio.Enabled)},
{Name: "BACKUP_CONFIGURATION_UPLOAD_REMOTE", Value: strconv.FormatBool(config.StorageEnabled())},
}
if fromPVC {
// Force PVC mode in the shell script, suppress local bucket
commonVar = append(commonVar, corev1.EnvVar{Name: "BACKUP_RESTORE_FROM_PVC", Value: "true"})
} else if config.Settings.LocalBucket != "" {
commonVar = append(commonVar, corev1.EnvVar{Name: "BACKUP_CONFIGURATION_LOCAL_BUCKET", Value: config.Settings.LocalBucket})
}
commonVar = append(commonVar, extraEnvVar...)
return commonVar
}

// buildVolumeMounts constructs volume mounts for the restore job container
func buildVolumeMounts() []corev1.VolumeMount {
return []corev1.VolumeMount{
func buildVolumeMounts(config *config.Config) []corev1.VolumeMount {
mounts := []corev1.VolumeMount{
{Name: "backup-log", MountPath: "/opt/docker/etc_log"},
{Name: "backup-restore-scripts", MountPath: "/backup-restore-scripts"},
{Name: "minio-keys", MountPath: "/aws-keys"},
{Name: "tmp-data", MountPath: "/tmp-data"},
{Name: "settings-backup-data", MountPath: "/settings-backup-data"},
}
// Mount PVC in legacy mode or when --from-pvc is set
if config.IsLegacyMode() || fromPVC {
mounts = append(mounts, corev1.VolumeMount{Name: "settings-backup-data", MountPath: "/settings-backup-data"})
}
return mounts
}

// buildVolumes constructs volumes for the restore job pod
func buildVolumes(config *config.Config, defaultMode int32) []corev1.Volume {
return []corev1.Volume{
volumes := []corev1.Volume{
{
Name: "backup-log",
VolumeSource: corev1.VolumeSource{
Expand All @@ -235,7 +252,7 @@ func buildVolumes(config *config.Config, defaultMode int32) []corev1.Volume {
Name: "minio-keys",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: restore.MinioKeysSecretName,
SecretName: restore.StorageKeysSecretName,
},
},
},
Expand All @@ -245,15 +262,19 @@ func buildVolumes(config *config.Config, defaultMode int32) []corev1.Volume {
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
}
// Include PVC volume in legacy mode or when --from-pvc is set
if config.IsLegacyMode() || fromPVC {
volumes = append(volumes, corev1.Volume{
Name: "settings-backup-data",
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: config.Settings.Restore.PVC,
},
},
},
})
}
return volumes
}

// buildContainers constructs containers for the restore job
Expand All @@ -266,6 +287,6 @@ func buildContainer(envVar []corev1.EnvVar, command []string, config *config.Con
Command: command,
Env: envVar,
Resources: k8s.ConvertResources(config.Settings.Restore.Job.Resources),
VolumeMounts: buildVolumeMounts(),
VolumeMounts: buildVolumeMounts(config),
}
}
2 changes: 1 addition & 1 deletion cmd/stackgraph/check_and_finalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Examples:
# Wait for job completion and cleanup
sts-backup stackgraph check-and-finalize --job stackgraph-restore-20250128t143000 --wait -n my-namespace`,
Run: func(_ *cobra.Command, _ []string) {
cmdutils.Run(globalFlags, runCheckAndFinalize, cmdutils.MinioIsRequired)
cmdutils.Run(globalFlags, runCheckAndFinalize, cmdutils.StorageIsRequired)
},
}

Expand Down
11 changes: 6 additions & 5 deletions cmd/stackgraph/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,17 @@ func listCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
Use: "list",
Short: "List available Stackgraph backups from S3/Minio",
Run: func(_ *cobra.Command, _ []string) {
cmdutils.Run(globalFlags, runList, cmdutils.MinioIsRequired)
cmdutils.Run(globalFlags, runList, cmdutils.StorageIsRequired)
},
}
}

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
// Setup port-forward to S3-compatible storage
storageService := appCtx.Config.GetStorageService()
serviceName := storageService.Name
localPort := storageService.LocalPortForwardPort
remotePort := storageService.Port

pf, err := portforward.SetupPortForward(appCtx.K8sClient, appCtx.Namespace, serviceName, localPort, remotePort, appCtx.Logger)
if err != nil {
Expand Down
19 changes: 11 additions & 8 deletions cmd/stackgraph/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func restoreCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command {
Short: "Restore Stackgraph from a backup archive",
Long: `Restore Stackgraph data from a backup archive stored in S3/Minio. Can use --latest or --archive to specify which backup to restore.`,
Run: func(_ *cobra.Command, _ []string) {
cmdutils.Run(globalFlags, runRestore, cmdutils.MinioIsRequired)
cmdutils.Run(globalFlags, runRestore, cmdutils.StorageIsRequired)
},
}

Expand Down Expand Up @@ -143,10 +143,11 @@ func waitAndCleanupRestoreJob(k8sClient *k8s.Client, namespace, jobName string,

// getLatestBackup retrieves the most recent backup from S3
func getLatestBackup(k8sClient *k8s.Client, namespace string, config *config.Config, log *logger.Logger) (string, error) {
// Setup port-forward to Minio
serviceName := config.Minio.Service.Name
localPort := config.Minio.Service.LocalPortForwardPort
remotePort := config.Minio.Service.Port
// Setup port-forward to S3-compatible storage
storageService := config.GetStorageService()
serviceName := storageService.Name
localPort := storageService.LocalPortForwardPort
remotePort := storageService.Port

pf, err := portforward.SetupPortForward(k8sClient, namespace, serviceName, localPort, remotePort, log)
if err != nil {
Expand All @@ -156,7 +157,7 @@ func getLatestBackup(k8sClient *k8s.Client, namespace string, config *config.Con

// Create S3 client
endpoint := fmt.Sprintf("http://localhost:%d", pf.LocalPort)
s3Client, err := s3client.NewClient(endpoint, config.Minio.AccessKey, config.Minio.SecretKey)
s3Client, err := s3client.NewClient(endpoint, config.GetStorageAccessKey(), config.GetStorageSecretKey())
if err != nil {
return "", err
}
Expand Down Expand Up @@ -261,13 +262,14 @@ func createRestoreJob(k8sClient *k8s.Client, namespace, jobName, backupFile stri

// buildRestoreEnvVars constructs environment variables for the restore job
func buildRestoreEnvVars(backupFile string, config *config.Config) []corev1.EnvVar {
storageService := config.GetStorageService()
return []corev1.EnvVar{
{Name: "BACKUP_FILE", Value: backupFile},
{Name: "FORCE_DELETE", Value: purgeStackgraphDataFlag},
{Name: "BACKUP_STACKGRAPH_BUCKET_NAME", Value: config.Stackgraph.Bucket},
{Name: "BACKUP_STACKGRAPH_S3_PREFIX", Value: config.Stackgraph.S3Prefix},
{Name: "BACKUP_STACKGRAPH_MULTIPART_ARCHIVE", Value: strconv.FormatBool(config.Stackgraph.MultipartArchive)},
{Name: "MINIO_ENDPOINT", Value: fmt.Sprintf("%s:%d", config.Minio.Service.Name, config.Minio.Service.Port)},
{Name: "MINIO_ENDPOINT", Value: fmt.Sprintf("%s:%d", storageService.Name, storageService.Port)},
{Name: "ZOOKEEPER_QUORUM", Value: config.Stackgraph.Restore.ZookeeperQuorum},
}
}
Expand All @@ -284,6 +286,7 @@ func buildRestoreVolumeMounts() []corev1.VolumeMount {

// buildRestoreInitContainers constructs init containers for the restore job
func buildRestoreInitContainers(config *config.Config) []corev1.Container {
storageService := config.GetStorageService()
return []corev1.Container{
{
Name: "wait",
Expand All @@ -292,7 +295,7 @@ func buildRestoreInitContainers(config *config.Config) []corev1.Container {
Command: []string{
"sh",
"-c",
fmt.Sprintf("/entrypoint -c %s:%d -t 300", config.Minio.Service.Name, config.Minio.Service.Port),
fmt.Sprintf("/entrypoint -c %s:%d -t 300", storageService.Name, storageService.Port),
},
SecurityContext: k8s.ConvertSecurityContext(config.Stackgraph.Restore.Job.ContainerSecurityContext),
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/victoriametrics/check_and_finalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Examples:
# Wait for job completion and cleanup
sts-backup victoriametrics check-and-finalize --job victoriametrics-restore-20250128t143000 --wait -n my-namespace`,
Run: func(_ *cobra.Command, _ []string) {
cmdutils.Run(globalFlags, runCheckAndFinalize, cmdutils.MinioIsRequired)
cmdutils.Run(globalFlags, runCheckAndFinalize, cmdutils.StorageIsRequired)
},
}

Expand Down
Loading
Loading