package util import ( "context" "fmt" "os" "path/filepath" "time" "github.com/spf13/cobra" "github.com/wild-cloud/wild-cli/internal/config" "github.com/wild-cloud/wild-cli/internal/environment" "github.com/wild-cloud/wild-cli/internal/external" "github.com/wild-cloud/wild-cli/internal/output" ) var ( backupAll bool ) func NewBackupCommand() *cobra.Command { cmd := &cobra.Command{ Use: "backup", Short: "Backup Wild Cloud system", Long: `Backup the entire Wild Cloud system including applications and data. This command performs a comprehensive backup of your Wild Cloud system using restic, including WC_HOME directory and all application data. Examples: wild backup wild backup --all`, RunE: runBackup, } cmd.Flags().BoolVar(&backupAll, "all", true, "backup all applications") return cmd } func runBackup(cmd *cobra.Command, args []string) error { output.Header("Wild Cloud System Backup") // Initialize environment env := environment.New() if err := env.RequiresProject(); err != nil { return err } // Check external tools toolManager := external.NewManager() if err := toolManager.CheckTools(cmd.Context(), []string{"restic"}); err != nil { return fmt.Errorf("required tools not available: %w", err) } // Load configuration configMgr := config.NewManager(env.ConfigPath(), env.SecretsPath()) // Get backup configuration backupRoot, err := configMgr.Get("cloud.backup.root") if err != nil || backupRoot == nil { return fmt.Errorf("backup root not configured. Set cloud.backup.root in config.yaml") } backupPassword, err := configMgr.GetSecret("cloud.backupPassword") if err != nil || backupPassword == nil { return fmt.Errorf("backup password not configured. Set cloud.backupPassword in secrets.yaml") } stagingDir, err := configMgr.Get("cloud.backup.staging") if err != nil || stagingDir == nil { return fmt.Errorf("backup staging directory not configured. Set cloud.backup.staging in config.yaml") } repository := fmt.Sprintf("%v", backupRoot) password := fmt.Sprintf("%v", backupPassword) staging := fmt.Sprintf("%v", stagingDir) output.Info("Backup repository: " + repository) // Initialize restic tool restic := toolManager.Restic() restic.SetRepository(repository) restic.SetPassword(password) // Check if repository exists, initialize if needed output.Info("Checking if restic repository exists...") if err := checkOrInitializeRepository(cmd.Context(), restic); err != nil { return fmt.Errorf("repository initialization failed: %w", err) } // Create staging directory if err := os.MkdirAll(staging, 0755); err != nil { return fmt.Errorf("creating staging directory: %w", err) } // Generate backup tags today := time.Now().Format("2006-01-02") tags := []string{"wild-cloud", "wc-home", today} // Backup entire WC_HOME output.Info("Backing up WC_HOME directory...") wcHome := env.WCHome() if wcHome == "" { wcHome = env.WildCloudDir() } if err := restic.Backup(cmd.Context(), []string{wcHome}, []string{".wildcloud/cache"}, tags); err != nil { return fmt.Errorf("backing up WC_HOME: %w", err) } output.Success("WC_HOME backup completed") // Backup applications if requested if backupAll { output.Info("Running backup for all applications...") if err := backupAllApplications(cmd.Context(), env, configMgr, restic, staging, today); err != nil { return fmt.Errorf("application backup failed: %w", err) } } // TODO: Future enhancements // - Backup Kubernetes resources (kubectl get all -A -o yaml) // - Backup persistent volumes // - Backup secrets and configmaps output.Success("Wild Cloud system backup completed successfully!") return nil } // checkOrInitializeRepository checks if restic repository exists and initializes if needed func checkOrInitializeRepository(ctx context.Context, restic *external.ResticTool) error { // Try to check repository if err := restic.Check(ctx); err != nil { output.Warning("No existing backup repository found. Initializing restic repository...") if err := restic.InitRepository(ctx); err != nil { return fmt.Errorf("initializing repository: %w", err) } output.Success("Repository initialized successfully") } else { output.Info("Using existing backup repository") } return nil } // backupAllApplications backs up all applications using the app backup functionality func backupAllApplications(ctx context.Context, env *environment.Environment, configMgr *config.Manager, restic *external.ResticTool, staging, dateTag string) error { // Get list of applications appsDir := env.AppsDir() if _, err := os.Stat(appsDir); os.IsNotExist(err) { output.Warning("No apps directory found, skipping application backups") return nil } entries, err := os.ReadDir(appsDir) if err != nil { return fmt.Errorf("reading apps directory: %w", err) } var apps []string for _, entry := range entries { if entry.IsDir() { apps = append(apps, entry.Name()) } } if len(apps) == 0 { output.Warning("No applications found, skipping application backups") return nil } output.Info(fmt.Sprintf("Found %d applications to backup: %v", len(apps), apps)) // For now, we'll use the existing bash script for application backups // This maintains compatibility with the existing backup infrastructure wcRoot := env.WCRoot() if wcRoot == "" { output.Warning("WC_ROOT not set, skipping application-specific backups") return nil } appBackupScript := filepath.Join(wcRoot, "bin", "wild-app-backup") if _, err := os.Stat(appBackupScript); os.IsNotExist(err) { output.Warning("App backup script not found, skipping application backups") return nil } // Execute the app backup script bashTool := external.NewBaseTool("bash", "bash") // Set environment variables needed by the script oldWCRoot := os.Getenv("WC_ROOT") oldWCHome := os.Getenv("WC_HOME") defer func() { if oldWCRoot != "" { _ = os.Setenv("WC_ROOT", oldWCRoot) } if oldWCHome != "" { _ = os.Setenv("WC_HOME", oldWCHome) } }() _ = os.Setenv("WC_ROOT", wcRoot) _ = os.Setenv("WC_HOME", env.WCHome()) output.Info("Running application backup script...") if _, err := bashTool.Execute(ctx, appBackupScript, "--all"); err != nil { output.Warning(fmt.Sprintf("Application backup script failed: %v", err)) return nil // Don't fail the entire backup for app backup issues } output.Success("Application backup script completed") // Upload each app's backup to restic individually stagingAppsDir := filepath.Join(staging, "apps") if _, err := os.Stat(stagingAppsDir); err != nil { output.Warning("No app staging directory found, skipping app backup uploads") return nil } entries, err = os.ReadDir(stagingAppsDir) if err != nil { output.Warning(fmt.Sprintf("Reading app staging directory failed: %v", err)) return nil } for _, entry := range entries { if !entry.IsDir() { continue } appName := entry.Name() appBackupDir := filepath.Join(stagingAppsDir, appName) output.Info(fmt.Sprintf("Uploading backup for app: %s", appName)) tags := []string{"wild-cloud", appName, dateTag} if err := restic.Backup(ctx, []string{appBackupDir}, []string{}, tags); err != nil { output.Warning(fmt.Sprintf("Failed to backup app %s: %v", appName, err)) continue } output.Success(fmt.Sprintf("Backup for app '%s' completed", appName)) } return nil }