diff --git a/api/v1/cluster_types.go b/api/v1/cluster_types.go index 3b7fd86cca..c1705cd4cd 100644 --- a/api/v1/cluster_types.go +++ b/api/v1/cluster_types.go @@ -1782,6 +1782,12 @@ type BootstrapRecovery struct { // created from scratch // +optional Secret *LocalObjectReference `json:"secret,omitempty"` + + // List of SQL queries to be executed as a superuser in the `postgres` + // database right after the recovery has been completed - to be used with extreme care + // (by default empty) + // +optional + PostRecoverySQL []string `json:"postRecoverySQL,omitempty"` } // DataSource contains the configuration required to bootstrap a diff --git a/internal/cmd/manager/instance/restore/cmd.go b/internal/cmd/manager/instance/restore/cmd.go index 2cdea6e1c8..b20e3b704f 100644 --- a/internal/cmd/manager/instance/restore/cmd.go +++ b/internal/cmd/manager/instance/restore/cmd.go @@ -25,6 +25,7 @@ import ( "os" "github.com/cloudnative-pg/machinery/pkg/log" + "github.com/kballard/go-shellquote" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" @@ -45,10 +46,11 @@ import ( // NewCmd creates the "restore" subcommand func NewCmd() *cobra.Command { var ( - clusterName string - namespace string - pgData string - pgWal string + clusterName string + namespace string + pgData string + pgWal string + postRecoverySQLStr string ) cmd := &cobra.Command{ @@ -57,6 +59,12 @@ func NewCmd() *cobra.Command { RunE: func(cmd *cobra.Command, _ []string) error { contextLogger := log.FromContext(cmd.Context()) + postRecoverySQL, err := shellquote.Split(postRecoverySQLStr) + if err != nil { + contextLogger.Error(err, "Error while parsing post recovery SQL queries") + return err + } + // Canceling this context ctx, cancel := context.WithCancel(cmd.Context()) defer cancel() @@ -84,12 +92,13 @@ func NewCmd() *cobra.Command { // Step 2: add the restore process to the manager restoreProcess := restoreRunnable{ - cli: mgr.GetClient(), - clusterName: clusterName, - namespace: namespace, - pgData: pgData, - pgWal: pgWal, - cancel: cancel, + cli: mgr.GetClient(), + clusterName: clusterName, + namespace: namespace, + pgData: pgData, + pgWal: pgWal, + cancel: cancel, + postRecoverySQL: postRecoverySQL, } if mgr.Add(&restoreProcess) != nil { contextLogger.Error(err, "while building the restore process") @@ -125,7 +134,7 @@ func NewCmd() *cobra.Command { "the cluster and the Pod in k8s") cmd.Flags().StringVar(&pgData, "pg-data", os.Getenv("PGDATA"), "The PGDATA to be restored") cmd.Flags().StringVar(&pgWal, "pg-wal", "", "The PGWAL to be restored") - + cmd.Flags().StringVar(&postRecoverySQLStr, "post-recovery-sql", "", "The SQL queries to be executed after the recovery on the postgres db") return cmd } diff --git a/internal/cmd/manager/instance/restore/restore.go b/internal/cmd/manager/instance/restore/restore.go index ad9d90e33f..a08cb36028 100644 --- a/internal/cmd/manager/instance/restore/restore.go +++ b/internal/cmd/manager/instance/restore/restore.go @@ -21,6 +21,7 @@ package restore import ( "context" + "database/sql" "errors" "fmt" "os" @@ -35,12 +36,13 @@ import ( ) type restoreRunnable struct { - cli client.Client - clusterName string - namespace string - pgData string - pgWal string - cancel context.CancelFunc + cli client.Client + clusterName string + namespace string + pgData string + pgWal string + postRecoverySQL []string + cancel context.CancelFunc } func (r *restoreRunnable) Start(ctx context.Context) error { @@ -59,7 +61,7 @@ func (r *restoreRunnable) Start(ctx context.Context) error { PgWal: r.pgWal, } - if err := restoreSubCommand(ctx, info, r.cli); err != nil { + if err := r.restoreSubCommand(ctx, info); err != nil { return fmt.Errorf("while restoring cluster: %s", err) } @@ -69,23 +71,49 @@ func (r *restoreRunnable) Start(ctx context.Context) error { return nil } -func restoreSubCommand(ctx context.Context, info postgres.InitInfo, cli client.Client) error { +func (r *restoreRunnable) restoreSubCommand(ctx context.Context, info postgres.InitInfo) error { contextLogger := log.FromContext(ctx) if err := info.EnsureTargetDirectoriesDoNotExist(ctx); err != nil { return err } - if err := info.Restore(ctx, cli); err != nil { + if err := info.Restore(ctx, r.cli); err != nil { contextLogger.Error(err, "Error while restoring a backup") cleanupDataDirectoryIfNeeded(ctx, err, info.PgData) return err } + if r.postRecoverySQL != nil { + instance := info.GetInstance() + + dbSuperUser, err := instance.GetSuperUserDB() + if err != nil { + return err + } + + log.Info("Executing post-recovery SQL") + if err := executeQueries(dbSuperUser, r.postRecoverySQL); err != nil { + return fmt.Errorf("error while executing post-recovery SQL: %w", err) + } + } + contextLogger.Info("restore command execution completed without errors") return nil } +func executeQueries(db *sql.DB, queries []string) error { + for _, sqlQuery := range queries { + log.Debug("Executing query", "sqlQuery", sqlQuery) + _, err := db.Exec(sqlQuery) + if err != nil { + return err + } + } + + return nil +} + func cleanupDataDirectoryIfNeeded(ctx context.Context, restoreError error, dataDirectory string) { contextLogger := log.FromContext(ctx) diff --git a/pkg/specs/jobs.go b/pkg/specs/jobs.go index 12ae77f200..7b22801b9d 100644 --- a/pkg/specs/jobs.go +++ b/pkg/specs/jobs.go @@ -211,6 +211,13 @@ func CreatePrimaryJobViaRecovery(cluster apiv1.Cluster, nodeSerial int, backup * "restore", } + if cluster.Spec.Bootstrap.Recovery.PostRecoverySQL != nil { + initCommand = append( + initCommand, + "--post-recovery-sql", + shellquote.Join(cluster.Spec.Bootstrap.Recovery.PostRecoverySQL...)) + } + initCommand = append(initCommand, buildCommonInitJobFlags(cluster)...) job := CreatePrimaryJob(cluster, nodeSerial, jobRoleFullRecovery, initCommand)