55package physical
66
77import (
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
1321const (
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.
1829type walg struct {
19- pgDataDir string
20- options walgOptions
30+ dockerClient * client.Client
31+ pgDataDir string
32+ parsedBackupName string
33+ options walgOptions
2134}
2235
2336type 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.
3552func (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+ }
0 commit comments