Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/v1/cluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 20 additions & 11 deletions internal/cmd/manager/instance/restore/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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{
Expand All @@ -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()
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}

Expand Down
46 changes: 37 additions & 9 deletions internal/cmd/manager/instance/restore/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package restore

import (
"context"
"database/sql"
"errors"
"fmt"
"os"
Expand All @@ -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 {
Expand All @@ -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)
}

Expand All @@ -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()
Comment on lines +87 to +89
Copy link
Author

@zhammer zhammer May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. no idea if either of these methods will succeed as the restore process is tapping into some of the shared initdb Info functionality without being bootstrapped the same way that initdb dbs are.
  2. i'm a little unclear how we'd know which user to use to run our statements against. since it's from a restore, we don't really know the user names, right? we don't know for sure if there is a root postgres user.

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
}

Comment on lines +105 to +116
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i intentionally do this here rather than in InitInfo (https://github.com/cloudnative-pg/cloudnative-pg/blob/2fb0189d4efa1a927684d30ebcdfa56e5f74e8ae/pkg/management/postgres/initdb.go#L421-L435) as it seems like the recovery steps are already using that interface a bit awkwardly

func cleanupDataDirectoryIfNeeded(ctx context.Context, restoreError error, dataDirectory string) {
contextLogger := log.FromContext(ctx)

Expand Down
7 changes: 7 additions & 0 deletions pkg/specs/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down