250 lines
7.2 KiB
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
|
|
}
|