feat: Add --clean-cluster flag for disaster recovery

Implements cluster cleanup option for CLI (matches TUI functionality).

Features:
- --clean-cluster flag drops all user databases before restore
- Preserves system databases (postgres, template0, template1)
- Shows databases to be dropped in dry-run mode
- Requires --confirm for safety
- Warns user with 🔥 icon when enabled
- Can combine with --workdir for full disaster recovery

Use cases:
- Disaster recovery scenarios (clean slate restore)
- Prevent database conflicts during cluster restore
- Ensure consistent cluster state

Examples:
  # Disaster recovery
  dbbackup restore cluster backup.tar.gz --clean-cluster --confirm

  # Combined with workdir
  dbbackup restore cluster backup.tar.gz \
    --clean-cluster \
    --workdir /mnt/storage/restore_tmp \
    --confirm

Chef's kiss backup tool! 👨‍🍳💋
This commit is contained in:
2025-11-28 13:55:02 +00:00
parent cfa51c4b37
commit 53b7c95abc
2 changed files with 91 additions and 6 deletions

View File

@@ -787,9 +787,22 @@ sudo -u postgres ./dbbackup restore cluster cluster_backup.tar.gz \
sudo -u postgres ./dbbackup restore cluster cluster_backup.tar.gz \ sudo -u postgres ./dbbackup restore cluster cluster_backup.tar.gz \
--workdir /mnt/storage/restore_tmp \ --workdir /mnt/storage/restore_tmp \
--confirm --confirm
# Disaster recovery: Drop all existing databases first (clean slate)
sudo -u postgres ./dbbackup restore cluster cluster_backup.tar.gz \
--clean-cluster \
--confirm
# Combined: Clean cluster + alternative storage
sudo -u postgres ./dbbackup restore cluster cluster_backup.tar.gz \
--clean-cluster \
--workdir /mnt/storage/restore_tmp \
--confirm
``` ```
**Note:** The `--workdir` flag is only needed when your system disk is small but you have larger mounted storage (NFS, SAN, etc.). For standard deployments, it's not required. **Note:**
- The `--workdir` flag is only needed when your system disk is small but you have larger mounted storage (NFS, SAN, etc.)
- The `--clean-cluster` flag drops all user databases before restore (keeps postgres, template0, template1). Use for disaster recovery scenarios.
**Safety Features:** **Safety Features:**

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -31,6 +32,7 @@ var (
restoreVerbose bool restoreVerbose bool
restoreNoProgress bool restoreNoProgress bool
restoreWorkdir string restoreWorkdir string
restoreCleanCluster bool
// Encryption flags // Encryption flags
restoreEncryptionKeyFile string restoreEncryptionKeyFile string
@@ -139,6 +141,9 @@ Examples:
# Use alternative working directory (for VMs with small system disk) # Use alternative working directory (for VMs with small system disk)
dbbackup restore cluster cluster_backup.tar.gz --workdir /mnt/storage/restore_tmp --confirm dbbackup restore cluster cluster_backup.tar.gz --workdir /mnt/storage/restore_tmp --confirm
# Disaster recovery: drop all existing databases first (clean slate)
dbbackup restore cluster cluster_backup.tar.gz --clean-cluster --confirm
`, `,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
RunE: runRestoreCluster, RunE: runRestoreCluster,
@@ -232,6 +237,7 @@ func init() {
restoreClusterCmd.Flags().BoolVar(&restoreConfirm, "confirm", false, "Confirm and execute restore (required)") restoreClusterCmd.Flags().BoolVar(&restoreConfirm, "confirm", false, "Confirm and execute restore (required)")
restoreClusterCmd.Flags().BoolVar(&restoreDryRun, "dry-run", false, "Show what would be done without executing") restoreClusterCmd.Flags().BoolVar(&restoreDryRun, "dry-run", false, "Show what would be done without executing")
restoreClusterCmd.Flags().BoolVar(&restoreForce, "force", false, "Skip safety checks and confirmations") restoreClusterCmd.Flags().BoolVar(&restoreForce, "force", false, "Skip safety checks and confirmations")
restoreClusterCmd.Flags().BoolVar(&restoreCleanCluster, "clean-cluster", false, "Drop all existing user databases before restore (disaster recovery)")
restoreClusterCmd.Flags().IntVar(&restoreJobs, "jobs", 0, "Number of parallel decompression jobs (0 = auto)") restoreClusterCmd.Flags().IntVar(&restoreJobs, "jobs", 0, "Number of parallel decompression jobs (0 = auto)")
restoreClusterCmd.Flags().StringVar(&restoreWorkdir, "workdir", "", "Working directory for extraction (use when system disk is small, e.g. /mnt/storage/restore_tmp)") restoreClusterCmd.Flags().StringVar(&restoreWorkdir, "workdir", "", "Working directory for extraction (use when system disk is small, e.g. /mnt/storage/restore_tmp)")
restoreClusterCmd.Flags().BoolVar(&restoreVerbose, "verbose", false, "Show detailed restore progress") restoreClusterCmd.Flags().BoolVar(&restoreVerbose, "verbose", false, "Show detailed restore progress")
@@ -519,6 +525,40 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error {
} }
} }
// Create database instance for pre-checks
db, err := database.New(cfg, log)
if err != nil {
return fmt.Errorf("failed to create database instance: %w", err)
}
defer db.Close()
// Check existing databases if --clean-cluster is enabled
var existingDBs []string
if restoreCleanCluster {
ctx := context.Background()
if err := db.Connect(ctx); err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
allDBs, err := db.ListDatabases(ctx)
if err != nil {
return fmt.Errorf("failed to list databases: %w", err)
}
// Filter out system databases (keep postgres, template0, template1)
systemDBs := map[string]bool{
"postgres": true,
"template0": true,
"template1": true,
}
for _, dbName := range allDBs {
if !systemDBs[dbName] {
existingDBs = append(existingDBs, dbName)
}
}
}
// Dry-run mode or confirmation required // Dry-run mode or confirmation required
isDryRun := restoreDryRun || !restoreConfirm isDryRun := restoreDryRun || !restoreConfirm
@@ -530,16 +570,27 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error {
if restoreWorkdir != "" { if restoreWorkdir != "" {
fmt.Printf(" Working Directory: %s (alternative extraction location)\n", restoreWorkdir) fmt.Printf(" Working Directory: %s (alternative extraction location)\n", restoreWorkdir)
} }
if restoreCleanCluster {
fmt.Printf(" Clean Cluster: true (will drop %d existing database(s))\n", len(existingDBs))
if len(existingDBs) > 0 {
fmt.Printf("\n⚠ Databases to be dropped:\n")
for _, dbName := range existingDBs {
fmt.Printf(" - %s\n", dbName)
}
}
}
fmt.Println("\nTo execute this restore, add --confirm flag") fmt.Println("\nTo execute this restore, add --confirm flag")
return nil return nil
} }
// Create database instance // Warning for clean-cluster
db, err := database.New(cfg, log) if restoreCleanCluster && len(existingDBs) > 0 {
if err != nil { log.Warn("🔥 Clean cluster mode enabled")
return fmt.Errorf("failed to create database instance: %w", err) log.Warn(fmt.Sprintf(" %d existing database(s) will be DROPPED before restore!", len(existingDBs)))
for _, dbName := range existingDBs {
log.Warn(" - " + dbName)
}
} }
defer db.Close()
// Create restore engine // Create restore engine
engine := restore.New(cfg, log, db) engine := restore.New(cfg, log, db)
@@ -558,6 +609,27 @@ func runRestoreCluster(cmd *cobra.Command, args []string) error {
cancel() cancel()
}() }()
// Drop existing databases if clean-cluster is enabled
if restoreCleanCluster && len(existingDBs) > 0 {
log.Info("Dropping existing databases before restore...")
for _, dbName := range existingDBs {
log.Info("Dropping database", "name", dbName)
// Use CLI-based drop to avoid connection issues
dropCmd := exec.CommandContext(ctx, "psql",
"-h", cfg.Host,
"-p", fmt.Sprintf("%d", cfg.Port),
"-U", cfg.User,
"-d", "postgres",
"-c", fmt.Sprintf("DROP DATABASE IF EXISTS \"%s\"", dbName),
)
if err := dropCmd.Run(); err != nil {
log.Warn("Failed to drop database", "name", dbName, "error", err)
// Continue with other databases
}
}
log.Info("Database cleanup completed")
}
// Execute cluster restore // Execute cluster restore
log.Info("Starting cluster restore...") log.Info("Starting cluster restore...")