diff --git a/cmd/cmdutils/common.go b/cmd/cmdutils/common.go index 98d5a1e..d0e5772 100644 --- a/cmd/cmdutils/common.go +++ b/cmd/cmdutils/common.go @@ -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 { diff --git a/cmd/settings/list.go b/cmd/settings/list.go index 265e0d5..b789bc5 100644 --- a/cmd/settings/list.go +++ b/cmd/settings/list.go @@ -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 { @@ -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 @@ -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 { @@ -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() diff --git a/cmd/settings/restore.go b/cmd/settings/restore.go index 2bd9fb5..4371210 100644 --- a/cmd/settings/restore.go +++ b/cmd/settings/restore.go @@ -44,6 +44,7 @@ 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") @@ -51,6 +52,11 @@ func restoreCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command { } 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 { @@ -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{ @@ -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, }, }, }, @@ -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 @@ -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), } } diff --git a/cmd/stackgraph/check_and_finalize.go b/cmd/stackgraph/check_and_finalize.go index 2895eb0..194657e 100644 --- a/cmd/stackgraph/check_and_finalize.go +++ b/cmd/stackgraph/check_and_finalize.go @@ -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) }, } diff --git a/cmd/stackgraph/list.go b/cmd/stackgraph/list.go index 330fa8c..9f309c6 100644 --- a/cmd/stackgraph/list.go +++ b/cmd/stackgraph/list.go @@ -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 { diff --git a/cmd/stackgraph/restore.go b/cmd/stackgraph/restore.go index 702704e..b8b0817 100644 --- a/cmd/stackgraph/restore.go +++ b/cmd/stackgraph/restore.go @@ -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) }, } @@ -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 { @@ -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 } @@ -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}, } } @@ -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", @@ -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), }, diff --git a/cmd/victoriametrics/check_and_finalize.go b/cmd/victoriametrics/check_and_finalize.go index a20b433..65a45f7 100644 --- a/cmd/victoriametrics/check_and_finalize.go +++ b/cmd/victoriametrics/check_and_finalize.go @@ -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) }, } diff --git a/cmd/victoriametrics/list.go b/cmd/victoriametrics/list.go index 839db9e..adad31c 100644 --- a/cmd/victoriametrics/list.go +++ b/cmd/victoriametrics/list.go @@ -27,16 +27,17 @@ func listCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command { Use: "list", Short: "List available VictoriaMetrics 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 { diff --git a/cmd/victoriametrics/restore.go b/cmd/victoriametrics/restore.go index 231eb81..878cbf0 100644 --- a/cmd/victoriametrics/restore.go +++ b/cmd/victoriametrics/restore.go @@ -40,7 +40,7 @@ func restoreCmd(globalFlags *config.CLIGlobalFlags) *cobra.Command { Short: "Restore VictoriaMetrics from a backup archive", Long: `Restore VictoriaMetrics 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) }, } @@ -141,10 +141,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 { @@ -154,7 +155,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 } @@ -228,8 +229,9 @@ func createRestoreJob(k8sClient *k8s.Client, namespace, jobName, backupFile stri // buildRestoreEnvVars constructs environment variables for the restore job func buildRestoreEnvVars(config *config.Config) []corev1.EnvVar { + storageService := config.GetStorageService() return []corev1.EnvVar{ - {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)}, } } @@ -252,7 +254,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", config.GetStorageService().Name, config.GetStorageService().Port), }, }, } diff --git a/internal/app/app.go b/internal/app/app.go index 7cd1d94..6f61c6e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -40,9 +40,10 @@ func NewContext(flags *config.CLIGlobalFlags) (*Context, error) { return nil, fmt.Errorf("failed to load configuration: %w", err) } - // Create S3 client - endpoint := fmt.Sprintf("http://localhost:%d", cfg.Minio.Service.LocalPortForwardPort) - s3Client, err := s3.NewClient(endpoint, cfg.Minio.AccessKey, cfg.Minio.SecretKey) + // Create S3 client using storage config (new mode) or minio config (legacy mode) + storageService := cfg.GetStorageService() + endpoint := fmt.Sprintf("http://localhost:%d", storageService.LocalPortForwardPort) + s3Client, err := s3.NewClient(endpoint, cfg.GetStorageAccessKey(), cfg.GetStorageSecretKey()) if err != nil { return nil, err } diff --git a/internal/foundation/config/config.go b/internal/foundation/config/config.go index 64e01d9..ee582cf 100644 --- a/internal/foundation/config/config.go +++ b/internal/foundation/config/config.go @@ -18,13 +18,54 @@ import ( type Config struct { Kubernetes KubernetesConfig `yaml:"kubernetes"` Elasticsearch ElasticsearchConfig `yaml:"elasticsearch" validate:"required"` - Minio MinioConfig `yaml:"minio" validate:"required"` + Minio MinioConfig `yaml:"minio"` + Storage StorageConfig `yaml:"storage"` Stackgraph StackgraphConfig `yaml:"stackgraph" validate:"required"` Settings SettingsConfig `yaml:"settings" validate:"required"` VictoriaMetrics VictoriaMetricsConfig `yaml:"victoriaMetrics" validate:"required"` Clickhouse ClickhouseConfig `yaml:"clickhouse" validate:"required"` } +// IsLegacyMode returns true when the configuration uses the legacy Minio config. +// Legacy mode is detected by the presence of the Minio config with a non-empty service name. +func (c *Config) IsLegacyMode() bool { + return c.Minio.Service.Name != "" +} + +// StorageEnabled returns true when S3-compatible storage is available, +// either through legacy Minio (with Enabled=true) or new Storage config. +func (c *Config) StorageEnabled() bool { + if c.IsLegacyMode() { + return c.Minio.Enabled + } + return c.Storage.GlobalBackupEnabled +} + +// GetStorageService returns the service config for the S3-compatible storage, +// using either Storage (new) or Minio (legacy) config. +func (c *Config) GetStorageService() ServiceConfig { + if c.IsLegacyMode() { + return c.Minio.Service + } + return c.Storage.Service +} + +// GetStorageAccessKey returns the access key for the S3-compatible storage. +func (c *Config) GetStorageAccessKey() string { + if c.IsLegacyMode() { + return c.Minio.AccessKey + } + return c.Storage.AccessKey +} + +// GetStorageSecretKey returns the secret key for the S3-compatible storage. +func (c *Config) GetStorageSecretKey() string { + if c.IsLegacyMode() { + return c.Minio.SecretKey + } + return c.Storage.SecretKey +} + // KubernetesConfig holds Kubernetes-wide configuration type KubernetesConfig struct { CommonLabels map[string]string `yaml:"commonLabels"` @@ -77,12 +118,20 @@ type ServiceConfig struct { LocalPortForwardPort int `yaml:"localPortForwardPort" validate:"required,min=1,max=65535"` } -// MinioConfig holds Minio-specific configuration +// MinioConfig holds Minio-specific configuration (legacy mode) type MinioConfig struct { Enabled bool `yaml:"enabled" validate:"boolean"` - Service ServiceConfig `yaml:"service" validate:"required"` - AccessKey string `yaml:"accessKey" validate:"required"` // From secret - SecretKey string `yaml:"secretKey" validate:"required"` // From secret + Service ServiceConfig `yaml:"service" validate:"omitempty"` + AccessKey string `yaml:"accessKey"` // From secret + SecretKey string `yaml:"secretKey"` // From secret +} + +// StorageConfig holds S3-compatible storage configuration (new mode, replaces Minio) +type StorageConfig struct { + GlobalBackupEnabled bool `yaml:"globalBackupEnabled" validate:"boolean"` + Service ServiceConfig `yaml:"service" validate:"omitempty"` + AccessKey string `yaml:"accessKey"` // From secret + SecretKey string `yaml:"secretKey"` // From secret } // StackgraphConfig holds Stackgraph backup-specific configuration @@ -121,9 +170,10 @@ type StackgraphRestoreConfig struct { } type SettingsConfig struct { - Bucket string `yaml:"bucket" validate:"required"` - S3Prefix string `yaml:"s3Prefix"` - Restore SettingsRestoreConfig `yaml:"restore" validate:"required"` + Bucket string `yaml:"bucket" validate:"required"` + S3Prefix string `yaml:"s3Prefix"` + LocalBucket string `yaml:"localBucket"` + Restore SettingsRestoreConfig `yaml:"restore" validate:"required"` } type SettingsRestoreConfig struct { @@ -134,7 +184,7 @@ type SettingsRestoreConfig struct { PlatformVersion string `yaml:"platformVersion" validate:"required"` ZookeeperQuorum string `yaml:"zookeeperQuorum" validate:"required"` Job JobConfig `yaml:"job" validate:"required"` - PVC string `yaml:"pvc" validate:"required"` + PVC string `yaml:"pvc"` // Required only in legacy mode } // ClickhouseConfig holds Clickhouse-specific configuration @@ -338,6 +388,21 @@ func LoadConfig(clientset kubernetes.Interface, namespace, configMapName, secret return nil, fmt.Errorf("configuration validation failed: %w", err) } + // Custom validation: either minio or storage must be configured + if config.Minio.Service.Name == "" && config.Storage.Service.Name == "" { + return nil, fmt.Errorf("configuration validation failed: either 'minio' or 'storage' must be configured") + } + + // In legacy mode (minio), PVC is required for settings + if config.IsLegacyMode() && config.Settings.Restore.PVC == "" { + return nil, fmt.Errorf("configuration validation failed: settings.restore.pvc is required in legacy (minio) mode") + } + + // In new mode (storage), localBucket is required for settings + if !config.IsLegacyMode() && config.Settings.LocalBucket == "" { + return nil, fmt.Errorf("configuration validation failed: settings.localBucket is required in storage mode") + } + return config, nil } diff --git a/internal/orchestration/restore/resources.go b/internal/orchestration/restore/resources.go index d5128c4..b37f341 100644 --- a/internal/orchestration/restore/resources.go +++ b/internal/orchestration/restore/resources.go @@ -10,8 +10,10 @@ import ( ) const ( - // MinioKeysSecretName is the name of the secret containing Minio access/secret keys - MinioKeysSecretName = "suse-observability-backup-cli-minio-keys" //nolint:gosec // This is a Kubernetes secret name, not a credential + // StorageKeysSecretName is the name of the secret containing S3-compatible storage access/secret keys + StorageKeysSecretName = "suse-observability-backup-cli-minio-keys" //nolint:gosec // This is a Kubernetes secret name, not a credential + // MinioKeysSecretName is an alias for StorageKeysSecretName for backward compatibility + MinioKeysSecretName = StorageKeysSecretName // RestoreScriptsConfigMap is the name of the ConfigMap containing restore scripts RestoreScriptsConfigMap = "suse-observability-backup-cli-restore-scripts" ) @@ -41,19 +43,19 @@ func EnsureResources(k8sClient *k8s.Client, namespace string, config *config.Con } log.Successf("Backup scripts ConfigMap ready") - // Ensure Minio keys secret exists - log.Infof("Ensuring Minio keys secret exists...") + // Ensure storage keys secret exists (uses storage or minio credentials) + log.Infof("Ensuring storage keys secret exists...") secretData := map[string][]byte{ - "accesskey": []byte(config.Minio.AccessKey), - "secretkey": []byte(config.Minio.SecretKey), + "accesskey": []byte(config.GetStorageAccessKey()), + "secretkey": []byte(config.GetStorageSecretKey()), } secretLabels := k8s.MergeLabels(config.Kubernetes.CommonLabels, map[string]string{}) - if _, err := k8sClient.EnsureSecret(namespace, MinioKeysSecretName, secretData, secretLabels); err != nil { - return fmt.Errorf("failed to ensure Minio keys secret: %w", err) + if _, err := k8sClient.EnsureSecret(namespace, StorageKeysSecretName, secretData, secretLabels); err != nil { + return fmt.Errorf("failed to ensure storage keys secret: %w", err) } - log.Successf("Minio keys secret ready") + log.Successf("Storage keys secret ready") return nil } diff --git a/internal/scripts/scripts/restore-settings-backup.sh b/internal/scripts/scripts/restore-settings-backup.sh index ed6531f..68b95c4 100644 --- a/internal/scripts/scripts/restore-settings-backup.sh +++ b/internal/scripts/scripts/restore-settings-backup.sh @@ -4,22 +4,53 @@ set -Eeuo pipefail export BACKUP_DIR=/settings-backup-data export TMP_DIR=/tmp-data -RESTORE_FILE="${BACKUP_DIR}/${BACKUP_FILE}" - -if [ "$BACKUP_CONFIGURATION_UPLOAD_REMOTE" == "true" ] && [ ! -f "${RESTORE_FILE}" ]; then +setup_aws_credentials() { export AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID="$(cat /aws-keys/accesskey)" export AWS_SECRET_ACCESS_KEY AWS_SECRET_ACCESS_KEY="$(cat /aws-keys/secretkey)" +} + +download_from_s3() { + local bucket="$1" + local prefix="$2" + local dest="$3" + echo "=== Downloading Settings backup \"${BACKUP_FILE}\" from bucket \"${bucket}\"..." + sts-toolbox aws s3 --endpoint "http://${MINIO_ENDPOINT}" --region minio cp "s3://${bucket}/${prefix}${BACKUP_FILE}" "${dest}/${BACKUP_FILE}" +} + +RESTORE_FILE="" + +if [ "${BACKUP_RESTORE_FROM_PVC:-}" == "true" ]; then + # --from-pvc mode: use legacy PVC directly, no S3 fallback + RESTORE_FILE="${BACKUP_DIR}/${BACKUP_FILE}" +elif [ -n "${BACKUP_CONFIGURATION_LOCAL_BUCKET:-}" ]; then + # New mode: no PVC, download from local bucket first, fall back to remote bucket + setup_aws_credentials + + if download_from_s3 "${BACKUP_CONFIGURATION_LOCAL_BUCKET}" "" "${TMP_DIR}"; then + RESTORE_FILE="${TMP_DIR}/${BACKUP_FILE}" + elif [ "${BACKUP_CONFIGURATION_UPLOAD_REMOTE}" == "true" ]; then + echo "=== Backup not found in local bucket, trying remote bucket..." + if download_from_s3 "${BACKUP_CONFIGURATION_BUCKET_NAME}" "${BACKUP_CONFIGURATION_S3_PREFIX}" "${TMP_DIR}"; then + RESTORE_FILE="${TMP_DIR}/${BACKUP_FILE}" + fi + fi +else + # Legacy mode: check PVC first, fall back to remote bucket + RESTORE_FILE="${BACKUP_DIR}/${BACKUP_FILE}" + + if [ "$BACKUP_CONFIGURATION_UPLOAD_REMOTE" == "true" ] && [ ! -f "${RESTORE_FILE}" ]; then + setup_aws_credentials - echo "=== Downloading Settings backup \"${BACKUP_FILE}\" from bucket \"${BACKUP_CONFIGURATION_BUCKET_NAME}\"..." - sts-toolbox aws s3 --endpoint "http://${MINIO_ENDPOINT}" --region minio cp "s3://${BACKUP_CONFIGURATION_BUCKET_NAME}/${BACKUP_CONFIGURATION_S3_PREFIX}${BACKUP_FILE}" "${TMP_DIR}/${BACKUP_FILE}" - RESTORE_FILE="${TMP_DIR}/${BACKUP_FILE}" + download_from_s3 "${BACKUP_CONFIGURATION_BUCKET_NAME}" "${BACKUP_CONFIGURATION_S3_PREFIX}" "${TMP_DIR}" + RESTORE_FILE="${TMP_DIR}/${BACKUP_FILE}" + fi fi -if [ ! -f "${RESTORE_FILE}" ]; then -echo "=== Backup file \"${RESTORE_FILE}\" not found, exiting..." -exit 1 +if [ -z "${RESTORE_FILE}" ] || [ ! -f "${RESTORE_FILE}" ]; then + echo "=== Backup file \"${BACKUP_FILE}\" not found, exiting..." + exit 1 fi echo "=== Restoring settings backup from \"${BACKUP_FILE}\"..."