package cmd import ( "context" "fmt" "os" "os/signal" "path/filepath" "syscall" "time" "dbbackup/internal/config" "dbbackup/internal/migrate" "github.com/spf13/cobra" ) var ( // Source connection flags migrateSourceHost string migrateSourcePort int migrateSourceUser string migrateSourcePassword string migrateSourceSSLMode string // Target connection flags migrateTargetHost string migrateTargetPort int migrateTargetUser string migrateTargetPassword string migrateTargetDatabase string migrateTargetSSLMode string // Migration options migrateWorkdir string migrateClean bool migrateConfirm bool migrateDryRun bool migrateKeepBackup bool migrateJobs int migrateVerbose bool migrateExclude []string ) // migrateCmd represents the migrate command var migrateCmd = &cobra.Command{ Use: "migrate", Short: "Migrate databases between servers", Long: `Migrate databases from one server to another. This command performs a staged migration: 1. Creates a backup from the source server 2. Stores backup in a working directory 3. Restores the backup to the target server 4. Cleans up temporary files (unless --keep-backup) Supports PostgreSQL and MySQL cluster migration or single database migration. Examples: # Migrate entire PostgreSQL cluster dbbackup migrate cluster \ --source-host old-server --source-port 5432 --source-user postgres \ --target-host new-server --target-port 5432 --target-user postgres \ --confirm # Migrate single database dbbackup migrate single mydb \ --source-host old-server --source-user postgres \ --target-host new-server --target-user postgres \ --confirm # Dry-run to preview migration dbbackup migrate cluster \ --source-host old-server \ --target-host new-server \ --dry-run `, Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, } // migrateClusterCmd migrates an entire database cluster var migrateClusterCmd = &cobra.Command{ Use: "cluster", Short: "Migrate entire database cluster to target server", Long: `Migrate all databases from source cluster to target server. This command: 1. Connects to source server and lists all databases 2. Creates individual backups of each database 3. Restores each database to target server 4. Optionally cleans up backup files after successful migration Requirements: - Database client tools (pg_dump/pg_restore or mysqldump/mysql) - Network access to both source and target servers - Sufficient disk space in working directory for backups Safety features: - Dry-run mode by default (use --confirm to execute) - Pre-flight checks on both servers - Optional backup retention after migration Examples: # Preview migration dbbackup migrate cluster \ --source-host old-server \ --target-host new-server # Execute migration with cleanup of existing databases dbbackup migrate cluster \ --source-host old-server --source-user postgres \ --target-host new-server --target-user postgres \ --clean --confirm # Exclude specific databases dbbackup migrate cluster \ --source-host old-server \ --target-host new-server \ --exclude template0,template1 \ --confirm `, RunE: runMigrateCluster, } // migrateSingleCmd migrates a single database var migrateSingleCmd = &cobra.Command{ Use: "single [database-name]", Short: "Migrate single database to target server", Long: `Migrate a single database from source server to target server. Examples: # Migrate database to same name on target dbbackup migrate single myapp_db \ --source-host old-server \ --target-host new-server \ --confirm # Migrate to different database name dbbackup migrate single myapp_db \ --source-host old-server \ --target-host new-server \ --target-database myapp_db_new \ --confirm `, Args: cobra.ExactArgs(1), RunE: runMigrateSingle, } func init() { // Add migrate command to root rootCmd.AddCommand(migrateCmd) // Add subcommands migrateCmd.AddCommand(migrateClusterCmd) migrateCmd.AddCommand(migrateSingleCmd) // Source connection flags migrateCmd.PersistentFlags().StringVar(&migrateSourceHost, "source-host", "localhost", "Source database host") migrateCmd.PersistentFlags().IntVar(&migrateSourcePort, "source-port", 5432, "Source database port") migrateCmd.PersistentFlags().StringVar(&migrateSourceUser, "source-user", "", "Source database user") migrateCmd.PersistentFlags().StringVar(&migrateSourcePassword, "source-password", "", "Source database password") migrateCmd.PersistentFlags().StringVar(&migrateSourceSSLMode, "source-ssl-mode", "prefer", "Source SSL mode (disable, prefer, require)") // Target connection flags migrateCmd.PersistentFlags().StringVar(&migrateTargetHost, "target-host", "", "Target database host (required)") migrateCmd.PersistentFlags().IntVar(&migrateTargetPort, "target-port", 5432, "Target database port") migrateCmd.PersistentFlags().StringVar(&migrateTargetUser, "target-user", "", "Target database user (default: same as source)") migrateCmd.PersistentFlags().StringVar(&migrateTargetPassword, "target-password", "", "Target database password") migrateCmd.PersistentFlags().StringVar(&migrateTargetSSLMode, "target-ssl-mode", "prefer", "Target SSL mode (disable, prefer, require)") // Single database specific flags migrateSingleCmd.Flags().StringVar(&migrateTargetDatabase, "target-database", "", "Target database name (default: same as source)") // Cluster specific flags migrateClusterCmd.Flags().StringSliceVar(&migrateExclude, "exclude", []string{}, "Databases to exclude from migration") // Migration options migrateCmd.PersistentFlags().StringVar(&migrateWorkdir, "workdir", "", "Working directory for backup files (default: system temp)") migrateCmd.PersistentFlags().BoolVar(&migrateClean, "clean", false, "Drop existing databases on target before restore") migrateCmd.PersistentFlags().BoolVar(&migrateConfirm, "confirm", false, "Confirm and execute migration (default: dry-run)") migrateCmd.PersistentFlags().BoolVar(&migrateDryRun, "dry-run", false, "Preview migration without executing") migrateCmd.PersistentFlags().BoolVar(&migrateKeepBackup, "keep-backup", false, "Keep backup files after successful migration") migrateCmd.PersistentFlags().IntVar(&migrateJobs, "jobs", 4, "Parallel jobs for backup/restore") migrateCmd.PersistentFlags().BoolVar(&migrateVerbose, "verbose", false, "Verbose output") // Mark required flags migrateCmd.MarkPersistentFlagRequired("target-host") } func runMigrateCluster(cmd *cobra.Command, args []string) error { // Validate target host if migrateTargetHost == "" { return fmt.Errorf("--target-host is required") } // Set defaults if migrateSourceUser == "" { migrateSourceUser = os.Getenv("USER") } if migrateTargetUser == "" { migrateTargetUser = migrateSourceUser } workdir := migrateWorkdir if workdir == "" { workdir = filepath.Join(os.TempDir(), "dbbackup-migrate") } // Create working directory if err := os.MkdirAll(workdir, 0755); err != nil { return fmt.Errorf("failed to create working directory: %w", err) } // Create source config sourceCfg := config.New() sourceCfg.Host = migrateSourceHost sourceCfg.Port = migrateSourcePort sourceCfg.User = migrateSourceUser sourceCfg.Password = migrateSourcePassword sourceCfg.SSLMode = migrateSourceSSLMode sourceCfg.Database = "postgres" // Default connection database sourceCfg.DatabaseType = cfg.DatabaseType sourceCfg.BackupDir = workdir sourceCfg.DumpJobs = migrateJobs // Create target config targetCfg := config.New() targetCfg.Host = migrateTargetHost targetCfg.Port = migrateTargetPort targetCfg.User = migrateTargetUser targetCfg.Password = migrateTargetPassword targetCfg.SSLMode = migrateTargetSSLMode targetCfg.Database = "postgres" targetCfg.DatabaseType = cfg.DatabaseType targetCfg.BackupDir = workdir // Create migration engine engine, err := migrate.NewEngine(sourceCfg, targetCfg, log) if err != nil { return fmt.Errorf("failed to create migration engine: %w", err) } defer engine.Close() // Configure engine engine.SetWorkDir(workdir) engine.SetKeepBackup(migrateKeepBackup) engine.SetJobs(migrateJobs) engine.SetDryRun(migrateDryRun || !migrateConfirm) engine.SetVerbose(migrateVerbose) engine.SetCleanTarget(migrateClean) // Setup context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Handle interrupt signals sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { <-sigChan log.Warn("Received interrupt signal, cancelling migration...") cancel() }() // Connect to databases if err := engine.Connect(ctx); err != nil { return fmt.Errorf("failed to connect: %w", err) } // Print migration plan fmt.Println() fmt.Println("=== Cluster Migration Plan ===") fmt.Println() fmt.Printf("Source: %s@%s:%d\n", migrateSourceUser, migrateSourceHost, migrateSourcePort) fmt.Printf("Target: %s@%s:%d\n", migrateTargetUser, migrateTargetHost, migrateTargetPort) fmt.Printf("Database Type: %s\n", cfg.DatabaseType) fmt.Printf("Working Directory: %s\n", workdir) fmt.Printf("Clean Target: %v\n", migrateClean) fmt.Printf("Keep Backup: %v\n", migrateKeepBackup) fmt.Printf("Parallel Jobs: %d\n", migrateJobs) if len(migrateExclude) > 0 { fmt.Printf("Excluded: %v\n", migrateExclude) } fmt.Println() isDryRun := migrateDryRun || !migrateConfirm if isDryRun { fmt.Println("Mode: DRY-RUN (use --confirm to execute)") fmt.Println() return engine.PreflightCheck(ctx) } fmt.Println("Mode: EXECUTE") fmt.Println() // Execute migration startTime := time.Now() result, err := engine.MigrateCluster(ctx, migrateExclude) duration := time.Since(startTime) if err != nil { log.Error("Migration failed", "error", err, "duration", duration) return fmt.Errorf("migration failed: %w", err) } // Print results fmt.Println() fmt.Println("=== Migration Complete ===") fmt.Println() fmt.Printf("Duration: %s\n", duration.Round(time.Second)) fmt.Printf("Databases Migrated: %d\n", result.DatabaseCount) if result.BackupPath != "" && migrateKeepBackup { fmt.Printf("Backup Location: %s\n", result.BackupPath) } fmt.Println() return nil } func runMigrateSingle(cmd *cobra.Command, args []string) error { dbName := args[0] // Validate target host if migrateTargetHost == "" { return fmt.Errorf("--target-host is required") } // Set defaults if migrateSourceUser == "" { migrateSourceUser = os.Getenv("USER") } if migrateTargetUser == "" { migrateTargetUser = migrateSourceUser } targetDB := migrateTargetDatabase if targetDB == "" { targetDB = dbName } workdir := migrateWorkdir if workdir == "" { workdir = filepath.Join(os.TempDir(), "dbbackup-migrate") } // Create working directory if err := os.MkdirAll(workdir, 0755); err != nil { return fmt.Errorf("failed to create working directory: %w", err) } // Create source config sourceCfg := config.New() sourceCfg.Host = migrateSourceHost sourceCfg.Port = migrateSourcePort sourceCfg.User = migrateSourceUser sourceCfg.Password = migrateSourcePassword sourceCfg.SSLMode = migrateSourceSSLMode sourceCfg.Database = dbName sourceCfg.DatabaseType = cfg.DatabaseType sourceCfg.BackupDir = workdir sourceCfg.DumpJobs = migrateJobs // Create target config targetCfg := config.New() targetCfg.Host = migrateTargetHost targetCfg.Port = migrateTargetPort targetCfg.User = migrateTargetUser targetCfg.Password = migrateTargetPassword targetCfg.SSLMode = migrateTargetSSLMode targetCfg.Database = targetDB targetCfg.DatabaseType = cfg.DatabaseType targetCfg.BackupDir = workdir // Create migration engine engine, err := migrate.NewEngine(sourceCfg, targetCfg, log) if err != nil { return fmt.Errorf("failed to create migration engine: %w", err) } defer engine.Close() // Configure engine engine.SetWorkDir(workdir) engine.SetKeepBackup(migrateKeepBackup) engine.SetJobs(migrateJobs) engine.SetDryRun(migrateDryRun || !migrateConfirm) engine.SetVerbose(migrateVerbose) engine.SetCleanTarget(migrateClean) // Setup context with cancellation ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Handle interrupt signals sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) go func() { <-sigChan log.Warn("Received interrupt signal, cancelling migration...") cancel() }() // Connect to databases if err := engine.Connect(ctx); err != nil { return fmt.Errorf("failed to connect: %w", err) } // Print migration plan fmt.Println() fmt.Println("=== Single Database Migration Plan ===") fmt.Println() fmt.Printf("Source: %s@%s:%d/%s\n", migrateSourceUser, migrateSourceHost, migrateSourcePort, dbName) fmt.Printf("Target: %s@%s:%d/%s\n", migrateTargetUser, migrateTargetHost, migrateTargetPort, targetDB) fmt.Printf("Database Type: %s\n", cfg.DatabaseType) fmt.Printf("Working Directory: %s\n", workdir) fmt.Printf("Clean Target: %v\n", migrateClean) fmt.Printf("Keep Backup: %v\n", migrateKeepBackup) fmt.Println() isDryRun := migrateDryRun || !migrateConfirm if isDryRun { fmt.Println("Mode: DRY-RUN (use --confirm to execute)") fmt.Println() return engine.PreflightCheck(ctx) } fmt.Println("Mode: EXECUTE") fmt.Println() // Execute migration startTime := time.Now() err = engine.MigrateSingle(ctx, dbName, targetDB) duration := time.Since(startTime) if err != nil { log.Error("Migration failed", "error", err, "duration", duration) return fmt.Errorf("migration failed: %w", err) } // Print results fmt.Println() fmt.Println("=== Migration Complete ===") fmt.Println() fmt.Printf("Duration: %s\n", duration.Round(time.Second)) fmt.Printf("Database: %s -> %s\n", dbName, targetDB) fmt.Println() return nil }