Skip to content

Commit 94ac19a

Browse files
committed
Merge branch '365-walg-sort' into 'master'
fix: get the name of the latest WAL-G backup (#365) Closes #365 See merge request postgres-ai/database-lab!540
2 parents ec26728 + 8e2d9c5 commit 94ac19a

File tree

7 files changed

+161
-12
lines changed

7 files changed

+161
-12
lines changed

engine/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ require (
6666
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
6767
github.com/sirupsen/logrus v1.8.1 // indirect
6868
github.com/stretchr/objx v0.2.0 // indirect
69+
golang.org/x/mod v0.5.1 // indirect
6970
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
7071
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
7172
golang.org/x/text v0.3.7 // indirect

engine/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
779779
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
780780
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
781781
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
782+
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
783+
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
782784
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
783785
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
784786
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package physical
66

77
import (
8+
"context"
9+
810
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/defaults"
911
)
1012

@@ -46,3 +48,8 @@ func (c *custom) GetRecoveryConfig(pgVersion float64) map[string]string {
4648

4749
return recoveryCfg
4850
}
51+
52+
// Init initialize custom recovery tool to work in provided container.
53+
func (c *custom) Init(ctx context.Context, containerID string) error {
54+
return nil
55+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package physical
66

77
import (
8+
"context"
89
"fmt"
910

1011
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/defaults"
@@ -54,3 +55,8 @@ func (p *pgbackrest) GetRecoveryConfig(pgVersion float64) map[string]string {
5455

5556
return recoveryCfg
5657
}
58+
59+
// Init initialize pgbackrest tool.
60+
func (p *pgbackrest) Init(ctx context.Context, containerID string) error {
61+
return nil
62+
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ type HealthCheck struct {
9393

9494
// restorer describes the interface of tools for physical restore.
9595
type restorer interface {
96+
97+
// Init initialize restore tooling inside of container.
98+
Init(ctx context.Context, containerID string) error
99+
96100
// GetRestoreCommand returns a command to restore data.
97101
GetRestoreCommand() string
98102

@@ -115,7 +119,7 @@ func NewJob(cfg config.JobConfig, global *global.Config, engineProps global.Engi
115119
return nil, errors.Wrap(err, "failed to unmarshal configuration options")
116120
}
117121

118-
restorer, err := physicalJob.getRestorer(physicalJob.Tool)
122+
restorer, err := physicalJob.getRestorer(cfg.Docker, physicalJob.Tool)
119123
if err != nil {
120124
return nil, errors.Wrap(err, "failed to init restorer")
121125
}
@@ -126,10 +130,10 @@ func NewJob(cfg config.JobConfig, global *global.Config, engineProps global.Engi
126130
}
127131

128132
// getRestorer builds a tool to perform physical restoring.
129-
func (r *RestoreJob) getRestorer(tool string) (restorer, error) {
133+
func (r *RestoreJob) getRestorer(client *client.Client, tool string) (restorer, error) {
130134
switch tool {
131135
case walgTool:
132-
return newWALG(r.fsPool.DataDir(), r.WALG), nil
136+
return newWALG(client, r.fsPool.DataDir(), r.WALG), nil
133137

134138
case pgbackrestTool:
135139
return newPgBackRest(r.PgBackRest), nil
@@ -215,6 +219,10 @@ func (r *RestoreJob) Run(ctx context.Context) (err error) {
215219
return errors.Wrapf(err, "failed to start container: %v", contID)
216220
}
217221

222+
if err := r.restorer.Init(ctx, contID); err != nil {
223+
return fmt.Errorf("failed to initialize restorer: %w", err)
224+
}
225+
218226
log.Msg("Running restore command: ", r.restorer.GetRestoreCommand())
219227
log.Msg(fmt.Sprintf("View logs using the command: %s %s", tools.ViewLogsCmd, r.restoreContainerName()))
220228

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

Lines changed: 126 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,52 @@
55
package physical
66

77
import (
8+
"context"
89
"fmt"
10+
"strings"
911

12+
"github.com/docker/docker/api/types"
13+
"github.com/docker/docker/client"
14+
"golang.org/x/mod/semver"
15+
16+
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools"
1017
"gitlab.com/postgres-ai/database-lab/v3/internal/retrieval/engine/postgres/tools/defaults"
18+
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
1119
)
1220

1321
const (
14-
walgTool = "walg"
22+
walgTool = "walg"
23+
walgSplitCount = 3
24+
walg11Version = "v1.1"
25+
latestBackup = "LATEST"
1526
)
1627

1728
// walg defines a WAL-G as an archival restoration tool.
1829
type walg struct {
19-
pgDataDir string
20-
options walgOptions
30+
dockerClient *client.Client
31+
pgDataDir string
32+
parsedBackupName string
33+
options walgOptions
2134
}
2235

2336
type walgOptions struct {
2437
BackupName string `yaml:"backupName"`
2538
}
2639

27-
func newWALG(pgDataDir string, options walgOptions) *walg {
28-
return &walg{
29-
pgDataDir: pgDataDir,
30-
options: options,
40+
func newWALG(dockerClient *client.Client, pgDataDir string, options walgOptions) *walg {
41+
walg := &walg{
42+
dockerClient: dockerClient,
43+
pgDataDir: pgDataDir,
44+
options: options,
45+
parsedBackupName: options.BackupName,
3146
}
47+
48+
return walg
3249
}
3350

3451
// GetRestoreCommand returns a command to restore data.
3552
func (w *walg) GetRestoreCommand() string {
36-
return fmt.Sprintf("wal-g backup-fetch %s %s", w.pgDataDir, w.options.BackupName)
53+
return fmt.Sprintf("wal-g backup-fetch %s %s", w.pgDataDir, w.parsedBackupName)
3754
}
3855

3956
// GetRecoveryConfig returns a recovery config to restore data.
@@ -48,3 +65,104 @@ func (w *walg) GetRecoveryConfig(pgVersion float64) map[string]string {
4865

4966
return recoveryCfg
5067
}
68+
69+
// Init initializes the wal-g tool to run in the provided container.
70+
func (w *walg) Init(ctx context.Context, containerID string) error {
71+
if strings.ToUpper(w.options.BackupName) != latestBackup {
72+
return nil
73+
}
74+
// workaround for issue with identification of last backup
75+
// https://gitlab.com/postgres-ai/database-lab/-/issues/365
76+
name, err := getLastBackupName(ctx, w.dockerClient, containerID)
77+
78+
if err != nil {
79+
return err
80+
}
81+
82+
if name != "" {
83+
w.parsedBackupName = name
84+
return nil
85+
}
86+
87+
return fmt.Errorf("failed to fetch last backup name from wal-g")
88+
}
89+
90+
// getLastBackupName returns the name of the latest backup from the wal-g backup list.
91+
func getLastBackupName(ctx context.Context, dockerClient *client.Client, containerID string) (string, error) {
92+
walgVersion, err := getWalgVersion(ctx, dockerClient, containerID)
93+
94+
if err != nil {
95+
return "", err
96+
}
97+
98+
result := semver.Compare(walgVersion, walg11Version)
99+
100+
// Try to fetch the latest backup with the details command for WAL-G 1.1 and higher
101+
if result >= 0 {
102+
output, err := parseLastBackupFromDetails(ctx, dockerClient, containerID)
103+
104+
if err == nil {
105+
return output, err
106+
}
107+
108+
// fallback to fetching last backup from list
109+
log.Err("Failed to parse last backup from wal-g details", err)
110+
}
111+
112+
return parseLastBackupFromList(ctx, dockerClient, containerID)
113+
}
114+
115+
// parseLastBackupFromList parses the name of the latest backup from "wal-g backup-list" output.
116+
func parseLastBackupFromList(ctx context.Context, dockerClient *client.Client, containerID string) (string, error) {
117+
output, err := tools.ExecCommandWithOutput(ctx, dockerClient, containerID, types.ExecConfig{
118+
Cmd: []string{"bash", "-c", "wal-g backup-list | grep base | sort -nk1 | tail -1 | awk '{print $1}'"},
119+
})
120+
if err != nil {
121+
return "", err
122+
}
123+
124+
log.Dbg("The latest WAL-G backup from the list", output)
125+
126+
return output, nil
127+
}
128+
129+
// parseLastBackupFromDetails parses the name of the latest backup from "wal-g backup-list --detail" output.
130+
func parseLastBackupFromDetails(ctx context.Context, dockerClient *client.Client, containerID string) (string, error) {
131+
output, err := tools.ExecCommandWithOutput(ctx, dockerClient, containerID, types.ExecConfig{
132+
Cmd: []string{"bash", "-c", "wal-g backup-list --detail | tail -1 | awk '{print $1}'"},
133+
})
134+
if err != nil {
135+
return "", err
136+
}
137+
138+
log.Dbg("The latest WAL-G backup from list details", output)
139+
140+
return output, nil
141+
}
142+
143+
// getWalgVersion fetches the WAL-G version installed in the provided container.
144+
func getWalgVersion(ctx context.Context, dockerClient *client.Client, containerID string) (string, error) {
145+
output, err := tools.ExecCommandWithOutput(ctx, dockerClient, containerID, types.ExecConfig{
146+
Cmd: []string{"bash", "-c", "wal-g --version"},
147+
})
148+
if err != nil {
149+
return "", err
150+
}
151+
152+
log.Dbg(output)
153+
154+
return parseWalGVersion(output)
155+
}
156+
157+
// parseWalGVersion extracts the version from the 'wal-g --version' output.
158+
// For example, "wal-g version v2.0.0 1eb88a5 2022.05.20_10:45:57 PostgreSQL".
159+
func parseWalGVersion(output string) (string, error) {
160+
walgVersion := strings.Split(output, "\t")
161+
versionParts := strings.Split(walgVersion[0], " ")
162+
163+
if len(versionParts) < walgSplitCount {
164+
return "", fmt.Errorf("failed to extract wal-g version number")
165+
}
166+
167+
return versionParts[walgSplitCount-1], nil
168+
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
)
88

99
func TestWALGRecoveryConfig(t *testing.T) {
10-
walg := newWALG("dataDir", walgOptions{})
10+
walg := newWALG(nil, "dataDir", walgOptions{})
1111

1212
recoveryConfig := walg.GetRecoveryConfig(11.7)
1313
expectedResponse11 := map[string]string{
@@ -22,3 +22,10 @@ func TestWALGRecoveryConfig(t *testing.T) {
2222
}
2323
assert.Equal(t, expectedResponse12, recoveryConfig)
2424
}
25+
26+
func TestWALGVersionParse(t *testing.T) {
27+
version, err := parseWalGVersion("wal-g version v2.0.0\t1eb88a5\t2022.05.20_10:45:57\tPostgreSQL")
28+
assert.NoError(t, err)
29+
assert.NotEmpty(t, version)
30+
assert.Equal(t, "v2.0.0", version)
31+
}

0 commit comments

Comments
 (0)