Files
wild-cloud/wild-cli/cmd/wild/util/backup.go

250 lines
7.2 KiB
Go

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
}