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:
15
README.md
15
README.md
@@ -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:**
|
||||||
|
|
||||||
|
|||||||
@@ -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...")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user