Skip to content

Commit 9b6409f

Browse files
committed
Merge branch 'feat/physical-restorer-custom-options' into 'master'
feat(retrieval): pass custom flags to physical restore tools Closes #760 See merge request postgres-ai/database-lab!1182
2 parents 4a5d3ab + 54921ee commit 9b6409f

7 files changed

Lines changed: 125 additions & 4 deletions

File tree

engine/configs/config.example.physical_pgbackrest.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ retrieval: # Data retrieval: initial sync and ongoing updates. Two methods:
9191
# standby_mode: on
9292
# recovery_target_timeline: 'latest'
9393

94+
# These envs reach the restore and sync containers only; the promotion container reads the
95+
# physicalSnapshot envs below, so anything WAL replay needs must be set in both places.
9496
envs: # Environment variables for pgBackRest; see https://pgbackrest.org/user-guide.html
9597
PGBACKREST_LOG_LEVEL_CONSOLE: detail # Log level; options: off, error, warn, info, detail, debug, trace
9698
PGBACKREST_PROCESS_MAX: 2 # Maximum number of processes to use for compression/decompression
@@ -111,6 +113,21 @@ retrieval: # Data retrieval: initial sync and ongoing updates. Two methods:
111113
pgbackrest: # pgBackRest specific configuration
112114
stanza: stanzaName # Stanza name (must match the stanza configured in your pgBackRest setup)
113115
delta: false # Use delta restore; set to true for incremental restores from last backup
116+
# Uncomment to append extra flags to the "pgbackrest restore" command.
117+
# The list is joined into a shell command line, so entries follow shell quoting rules: quote a value
118+
# containing whitespace or glob characters.
119+
# Do not repeat --type, --stanza, --pg1-path or --delta: pgBackRest rejects a single-valued option set
120+
# twice ("cannot be set multiple times"). List options such as --db-include may be repeated freely.
121+
# --recovery-option may be repeated too, but where it ends up depends on the Postgres version, and
122+
# neither outcome is what a reader expects. On 12 and newer pgBackRest writes it to
123+
# postgresql.auto.conf, read after the recovery config DBLab generates, so it overrides DBLab's
124+
# restore_command; on 11 and older it goes to recovery.conf, which DBLab then truncates and rewrites,
125+
# so it is dropped without a word. Never set recovery_target* through it either way - promotion relies
126+
# on recovery_target=immediate with recovery_target_action=promote.
127+
# Choose a repository with the PGBACKREST_REPO env rather than --repo, since restore and archive-get
128+
# both read it.
129+
# customOptions:
130+
# - "--db-include=mydb"
114131

115132
physicalSnapshot:
116133
options:

engine/configs/config.example.physical_walg.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,24 @@ retrieval: # Data retrieval: initial sync and ongoing updates. Two methods:
9191
# standby_mode: on
9292
# recovery_target_timeline: 'latest'
9393

94+
# These envs reach the restore and sync containers only; the promotion container reads the
95+
# physicalSnapshot envs below, so anything WAL replay needs must be set in both places.
9496
envs: # Environment variables for WAL-G; see https://github.com/wal-g/wal-g/blob/master/docs/README.md
9597
WALG_GS_PREFIX: "gs://{BUCKET}/{SCOPE}" # Google Storage prefix for WAL-G backups
9698
GOOGLE_APPLICATION_CREDENTIALS: "/tmp/sa.json" # Path to Google service account credentials
9799

98100
walg: # WAL-G specific configuration
99101
backupName: LATEST # Which backup to restore; use "LATEST" for most recent backup
102+
# Uncomment to append extra flags to the "wal-g backup-fetch" command, for options wal-g exposes
103+
# only as flags (--mask, --restore-spec, --restore-only); anything with a WALG_* variable belongs in envs.
104+
# The list is joined into a shell command line, so entries follow shell quoting rules: quote a value
105+
# containing whitespace or glob characters, e.g. - '--restore-only=my_db,"another db"'.
106+
# The flags reach backup-fetch alone - not the backup-list that resolves LATEST, and not the wal-fetch
107+
# in restore_command - so choose a storage with WALG_TARGET_STORAGE, which all three read, not --target-storage.
108+
# --target-user-data cannot be combined with backupName, in either form: wal-g rejects the flag and
109+
# the WALG_FETCH_TARGET_USER_DATA env alike when a backup name is also given.
110+
# customOptions:
111+
# - "--restore-only=mydb"
100112

101113
physicalSnapshot:
102114
options:

engine/internal/retrieval/engine/postgres/physical/pgbackrest.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package physical
77
import (
88
"context"
99
"fmt"
10+
"strings"
1011

1112
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/defaults"
1213
)
@@ -21,8 +22,9 @@ type pgbackrest struct {
2122
}
2223

2324
type pgbackrestOptions struct {
24-
Stanza string `yaml:"stanza"`
25-
Delta bool `yaml:"delta"`
25+
Stanza string `yaml:"stanza"`
26+
Delta bool `yaml:"delta"`
27+
CustomOptions []string `yaml:"customOptions"`
2628
}
2729

2830
func newPgBackRest(options pgbackrestOptions) *pgbackrest {
@@ -40,6 +42,10 @@ func (p *pgbackrest) GetRestoreCommand() string {
4042
restoreCmd += " --delta"
4143
}
4244

45+
if len(p.options.CustomOptions) > 0 {
46+
restoreCmd += " " + strings.Join(p.options.CustomOptions, " ")
47+
}
48+
4349
return restoreCmd
4450
}
4551

engine/internal/retrieval/engine/postgres/physical/pgbackrest_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,31 @@ func TestPgBackRestRestoreCommand_DifferentStanzas(t *testing.T) {
6666
}
6767
}
6868

69+
func TestPgBackRestRestoreCommand_CustomOptions(t *testing.T) {
70+
baseCmd := "sudo -Eu postgres pgbackrest --type=standby --pg1-path=${PGDATA} --stanza=stanzaName restore " +
71+
"--recovery-option=restore_command='pgbackrest --pg1-path=${PGDATA} --stanza=stanzaName archive-get %f %p'"
72+
73+
testCases := []struct {
74+
name string
75+
delta bool
76+
customOptions []string
77+
expected string
78+
}{
79+
{name: "no custom options", customOptions: nil, expected: baseCmd},
80+
{name: "empty custom options", customOptions: []string{}, expected: baseCmd},
81+
{name: "single option", customOptions: []string{"--db-include=mydb"}, expected: baseCmd + " --db-include=mydb"},
82+
{name: "multiple options", customOptions: []string{"--db-include=mydb", "--process-max=4"}, expected: baseCmd + " --db-include=mydb --process-max=4"},
83+
{name: "appended after delta", delta: true, customOptions: []string{"--db-include=mydb"}, expected: baseCmd + " --delta --db-include=mydb"},
84+
}
85+
86+
for _, tc := range testCases {
87+
t.Run(tc.name, func(t *testing.T) {
88+
p := newPgBackRest(pgbackrestOptions{Stanza: "stanzaName", Delta: tc.delta, CustomOptions: tc.customOptions})
89+
assert.Equal(t, tc.expected, p.GetRestoreCommand())
90+
})
91+
}
92+
}
93+
6994
func TestPgBackRestRecoveryConfig_VersionBoundary(t *testing.T) {
7095
p := newPgBackRest(pgbackrestOptions{Stanza: "mydb"})
7196

engine/internal/retrieval/engine/postgres/physical/physical_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111

1212
"github.com/stretchr/testify/assert"
1313
"github.com/stretchr/testify/require"
14+
15+
"gitlab.com/postgres-ai/database-lab/v3/internal/provision/resources"
1416
)
1517

1618
func TestInitParamsExtraction(t *testing.T) {
@@ -83,3 +85,33 @@ some_other_param setting: value
8385
require.NoError(t, err)
8486
assert.Empty(t, settings)
8587
}
88+
89+
func TestRestoreJobReload_CustomOptions(t *testing.T) {
90+
r := &RestoreJob{fsPool: &resources.Pool{MountDir: "/", PoolDirName: "pgdata"}}
91+
92+
err := r.Reload(map[string]interface{}{
93+
"tool": walgTool,
94+
"walg": map[string]interface{}{
95+
"backupName": "LATEST",
96+
"customOptions": []interface{}{"--mask", "'base/*'"},
97+
},
98+
"pgbackrest": map[string]interface{}{
99+
"stanza": "main",
100+
"customOptions": []interface{}{"--db-include=mydb"},
101+
},
102+
})
103+
require.NoError(t, err)
104+
105+
assert.Equal(t, []string{"--mask", "'base/*'"}, r.WALG.CustomOptions)
106+
assert.Equal(t, []string{"--db-include=mydb"}, r.PgBackRest.CustomOptions)
107+
108+
walgRestorer, err := r.getRestorer(nil, walgTool)
109+
require.NoError(t, err)
110+
assert.Equal(t, "wal-g backup-fetch /pgdata LATEST --mask 'base/*'", walgRestorer.GetRestoreCommand())
111+
112+
pgBackRestRestorer, err := r.getRestorer(nil, pgbackrestTool)
113+
require.NoError(t, err)
114+
assert.Equal(t, "sudo -Eu postgres pgbackrest --type=standby --pg1-path=${PGDATA} --stanza=main restore "+
115+
"--recovery-option=restore_command='pgbackrest --pg1-path=${PGDATA} --stanza=main archive-get %f %p' --db-include=mydb",
116+
pgBackRestRestorer.GetRestoreCommand())
117+
}

engine/internal/retrieval/engine/postgres/physical/wal_g.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ type walg struct {
3333
}
3434

3535
type walgOptions struct {
36-
BackupName string `yaml:"backupName"`
36+
BackupName string `yaml:"backupName"`
37+
CustomOptions []string `yaml:"customOptions"`
3738
}
3839

3940
func newWALG(dockerClient *client.Client, pgDataDir string, options walgOptions) *walg {
@@ -49,7 +50,13 @@ func newWALG(dockerClient *client.Client, pgDataDir string, options walgOptions)
4950

5051
// GetRestoreCommand returns a command to restore data.
5152
func (w *walg) GetRestoreCommand() string {
52-
return fmt.Sprintf("wal-g backup-fetch %s %s", w.pgDataDir, w.parsedBackupName)
53+
restoreCmd := fmt.Sprintf("wal-g backup-fetch %s %s", w.pgDataDir, w.parsedBackupName)
54+
55+
if len(w.options.CustomOptions) > 0 {
56+
restoreCmd += " " + strings.Join(w.options.CustomOptions, " ")
57+
}
58+
59+
return restoreCmd
5360
}
5461

5562
// GetRecoveryConfig returns a recovery config to restore data.

engine/internal/retrieval/engine/postgres/physical/wal_g_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@ func TestWALGGetRestoreCommand(t *testing.T) {
5151
}
5252
}
5353

54+
func TestWALGGetRestoreCommand_CustomOptions(t *testing.T) {
55+
testCases := []struct {
56+
name string
57+
customOptions []string
58+
expected string
59+
}{
60+
{name: "no custom options", customOptions: nil, expected: "wal-g backup-fetch /pgdata LATEST"},
61+
{name: "empty custom options", customOptions: []string{}, expected: "wal-g backup-fetch /pgdata LATEST"},
62+
{name: "single option", customOptions: []string{"--reverse-unpack"}, expected: "wal-g backup-fetch /pgdata LATEST --reverse-unpack"},
63+
{name: "option with value", customOptions: []string{"--mask", "'base/*'"}, expected: "wal-g backup-fetch /pgdata LATEST --mask 'base/*'"},
64+
{name: "multiple options", customOptions: []string{"--reverse-unpack", "--skip-redundant-tars"}, expected: "wal-g backup-fetch /pgdata LATEST --reverse-unpack --skip-redundant-tars"},
65+
{name: "quotes are passed through unmodified", customOptions: []string{`--restore-only=my_db,"another db"`}, expected: `wal-g backup-fetch /pgdata LATEST --restore-only=my_db,"another db"`},
66+
}
67+
68+
for _, tc := range testCases {
69+
t.Run(tc.name, func(t *testing.T) {
70+
w := newWALG(nil, "/pgdata", walgOptions{BackupName: "LATEST", CustomOptions: tc.customOptions})
71+
assert.Equal(t, tc.expected, w.GetRestoreCommand())
72+
})
73+
}
74+
}
75+
5476
func TestWALGVersionParse_TableDriven(t *testing.T) {
5577
testCases := []struct {
5678
name string

0 commit comments

Comments
 (0)