Skip to content

Commit d3ccd39

Browse files
committed
Merge branch '270-adjust-logical-configs' into 'master'
feat: adjust postgresql configuration before logical restore (#270): * add init and start Postgres functions to run with extended images * provide ability to adjust PostgreSQL configuration for logical dump/restore jobs Closes #270 See merge request postgres-ai/database-lab!303
2 parents d44f640 + a787c2c commit d3ccd39

File tree

7 files changed

+244
-43
lines changed

7 files changed

+244
-43
lines changed

configs/config.example.logical_generic.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ retrieval:
190190
# # Restore data even if the Postgres directory (`global.dataDir`) is not empty.
191191
# # Note the existing data might be overwritten.
192192
# forceInit: false
193+
#
194+
# configs:
195+
# shared_preload_libraries: "pg_stat_statements"
193196

194197
# Restores PostgreSQL database from the provided dump. If you use this block, do not use
195198
# "restore" option in the "logicalDump" job.
@@ -216,6 +219,10 @@ retrieval:
216219
# tables:
217220
# - test
218221

222+
#
223+
# configs:
224+
# shared_preload_libraries: "pg_stat_statements"
225+
219226
logicalSnapshot:
220227
options:
221228
# Define pre-precessing SQL queries for data patching. For example, "/tmp/scripts/sql".

configs/config.example.logical_rds_iam.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ retrieval:
191191
# # Restore data even if the Postgres directory (`global.dataDir`) is not empty.
192192
# # Note the existing data might be overwritten.
193193
# forceInit: false
194+
#
195+
# configs:
196+
# shared_preload_libraries: "pg_stat_statements"
194197

195198
# Restores PostgreSQL database from the provided dump. If you use this block, do not use
196199
# "restore" option in the "logicalDump" job.
@@ -216,6 +219,10 @@ retrieval:
216219
# tables:
217220
# - test
218221

222+
#
223+
# configs:
224+
# shared_preload_libraries: "pg_stat_statements"
225+
219226
logicalSnapshot:
220227
options:
221228
# Define pre-precessing SQL queries for data patching. For example, "/tmp/scripts/sql".

pkg/retrieval/engine/postgres/logical/dump.go

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,17 @@ import (
88
"context"
99
"fmt"
1010
"os"
11+
"path"
1112
"strconv"
1213
"strings"
1314
"time"
1415

1516
"github.com/docker/docker/api/types"
1617
"github.com/docker/docker/api/types/container"
18+
"github.com/docker/docker/api/types/mount"
1719
"github.com/docker/docker/api/types/network"
1820
"github.com/docker/docker/client"
1921
"github.com/pkg/errors"
20-
2122
"gitlab.com/postgres-ai/database-lab/v2/pkg/config/global"
2223
"gitlab.com/postgres-ai/database-lab/v2/pkg/log"
2324
"gitlab.com/postgres-ai/database-lab/v2/pkg/retrieval/config"
@@ -27,6 +28,7 @@ import (
2728
"gitlab.com/postgres-ai/database-lab/v2/pkg/retrieval/engine/postgres/tools/defaults"
2829
"gitlab.com/postgres-ai/database-lab/v2/pkg/retrieval/engine/postgres/tools/health"
2930
"gitlab.com/postgres-ai/database-lab/v2/pkg/retrieval/options"
31+
"gitlab.com/postgres-ai/database-lab/v2/pkg/services/provision/databases/postgres/pgconfig"
3032
"gitlab.com/postgres-ai/database-lab/v2/pkg/services/provision/resources"
3133
)
3234

@@ -109,7 +111,8 @@ type Connection struct {
109111

110112
// ImmediateRestore contains options for direct data restore without saving the dump file on disk.
111113
type ImmediateRestore struct {
112-
ForceInit bool `yaml:"forceInit"`
114+
ForceInit bool `yaml:"forceInit"`
115+
Configs map[string]string `yaml:"configs"`
113116
}
114117

115118
// NewDumpJob creates a new DumpJob.
@@ -230,6 +233,10 @@ func (d *DumpJob) Run(ctx context.Context) (err error) {
230233
return errors.Wrap(err, "failed to scan pulling image response")
231234
}
232235

236+
if err := os.MkdirAll(d.DumpOptions.DumpLocation, 0666); err != nil {
237+
return errors.Wrap(err, "failed to create a location directory")
238+
}
239+
233240
hostConfig, err := d.buildHostConfig(ctx)
234241
if err != nil {
235242
return errors.Wrap(err, "failed to build container host config")
@@ -263,14 +270,29 @@ func (d *DumpJob) Run(ctx context.Context) (err error) {
263270
return errors.Wrapf(err, "failed to start container %q", d.dumpContainerName())
264271
}
265272

273+
if err := d.setupConnectionOptions(ctx); err != nil {
274+
return errors.Wrap(err, "failed to setup connection options")
275+
}
276+
266277
log.Msg("Waiting for container readiness")
267278

279+
dataDir := d.fsPool.DataDir()
280+
268281
if err := tools.CheckContainerReadiness(ctx, d.dockerClient, dumpCont.ID); err != nil {
269-
return errors.Wrap(err, "failed to readiness check")
282+
var errHealthCheck *tools.ErrHealthCheck
283+
if !errors.As(err, &errHealthCheck) {
284+
return errors.Wrap(err, "failed to readiness check")
285+
}
286+
287+
if err := setupPGData(ctx, d.dockerClient, dataDir, dumpCont.ID); err != nil {
288+
return errors.Wrap(err, "failed to set up Postgres data")
289+
}
270290
}
271291

272-
if err := d.setupConnectionOptions(ctx); err != nil {
273-
return errors.Wrap(err, "failed to setup connection options")
292+
if d.DumpOptions.Restore != nil && len(d.DumpOptions.Restore.Configs) > 0 {
293+
if err := updateConfigs(ctx, d.dockerClient, dataDir, dumpCont.ID, d.DumpOptions.Restore.Configs); err != nil {
294+
return errors.Wrap(err, "failed to update configs")
295+
}
274296
}
275297

276298
dumpCommand := d.buildLogicalDumpCommand()
@@ -281,9 +303,10 @@ func (d *DumpJob) Run(ctx context.Context) (err error) {
281303
}
282304

283305
if d.DumpOptions.DumpLocation != "" && d.DumpOptions.Restore == nil {
284-
if err := tools.ExecCommand(ctx, d.dockerClient, dumpCont.ID, types.ExecConfig{
285-
Cmd: []string{"rm", "-rf", d.DumpOptions.DumpLocation},
306+
if out, err := tools.ExecCommandWithOutput(ctx, d.dockerClient, dumpCont.ID, types.ExecConfig{
307+
Cmd: []string{"rm", "-rf", path.Join(d.DumpOptions.DumpLocation, "*")},
286308
}); err != nil {
309+
log.Dbg(out)
287310
return errors.Wrap(err, "failed to clean up dump location")
288311
}
289312
}
@@ -317,6 +340,65 @@ func (d *DumpJob) Run(ctx context.Context) (err error) {
317340
return nil
318341
}
319342

343+
func setupPGData(ctx context.Context, dockerClient *client.Client, dataDir string, dumpContID string) error {
344+
isEmpty, err := tools.IsEmptyDirectory(dataDir)
345+
if err != nil {
346+
return errors.Wrap(err, "failed to explore the data directory")
347+
}
348+
349+
if !isEmpty {
350+
return nil
351+
}
352+
353+
if err := tools.ExecCommand(ctx, dockerClient, dumpContID, types.ExecConfig{
354+
Cmd: []string{"chown", "-R", "postgres", dataDir},
355+
}); err != nil {
356+
return errors.Wrap(err, "failed to set permissions")
357+
}
358+
359+
if err := tools.InitDB(ctx, dockerClient, dumpContID, dataDir); err != nil {
360+
return errors.Wrap(err, "failed to init Postgres")
361+
}
362+
363+
log.Dbg("Database has been initialized")
364+
365+
if err := tools.StartPostgres(ctx, dockerClient, dumpContID, dataDir, tools.DefaultStopTimeout); err != nil {
366+
return errors.Wrap(err, "failed to init Postgres")
367+
}
368+
369+
log.Dbg("Postgres has been started")
370+
371+
return nil
372+
}
373+
374+
func updateConfigs(ctx context.Context, dockerClient *client.Client, dataDir, contID string, configs map[string]string) error {
375+
log.Dbg("Stopping container to update configuration")
376+
377+
tools.StopContainer(ctx, dockerClient, contID, cont.StopTimeout)
378+
379+
// Run basic PostgreSQL configuration.
380+
cfgManager, err := pgconfig.NewCorrector(dataDir)
381+
if err != nil {
382+
return errors.Wrap(err, "failed to create a config manager")
383+
}
384+
385+
if err := cfgManager.AppendGeneralConfig(configs); err != nil {
386+
return errors.Wrap(err, "failed to append general configuration")
387+
}
388+
389+
if err := dockerClient.ContainerStart(ctx, contID, types.ContainerStartOptions{}); err != nil {
390+
return errors.Wrapf(err, "failed to start container %q", contID)
391+
}
392+
393+
log.Dbg("Waiting for container readiness")
394+
395+
if err := tools.CheckContainerReadiness(ctx, dockerClient, contID); err != nil {
396+
return errors.Wrap(err, "failed to readiness check")
397+
}
398+
399+
return nil
400+
}
401+
320402
// setupConnectionOptions prepares connection options to perform a logical dump.
321403
func (d *DumpJob) setupConnectionOptions(ctx context.Context) error {
322404
d.config.db = d.DumpOptions.Source.Connection
@@ -343,10 +425,7 @@ func (d *DumpJob) getEnvironmentVariables(password string) []string {
343425
"POSTGRES_PASSWORD=" + password,
344426
}
345427

346-
// Avoid initialization of PostgreSQL directory in case of preparing of a dump.
347-
if d.DumpOptions.Restore != nil {
348-
envs = append(envs, "PGDATA="+d.fsPool.DataDir())
349-
}
428+
envs = append(envs, "PGDATA="+d.fsPool.DataDir())
350429

351430
if d.DumpOptions.Source.Type == sourceTypeLocal && d.DumpOptions.Source.Connection.Port == defaults.Port {
352431
log.Msg(fmt.Sprintf("The default PostgreSQL port is busy, trying to use an alternative one: %d", reservePort))
@@ -377,6 +456,12 @@ func (d *DumpJob) buildHostConfig(ctx context.Context) (*container.HostConfig, e
377456
return nil, err
378457
}
379458

459+
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
460+
Type: mount.TypeBind,
461+
Source: d.DumpOptions.DumpLocation,
462+
Target: d.DumpOptions.DumpLocation,
463+
})
464+
380465
return hostConfig, nil
381466
}
382467

@@ -418,7 +503,7 @@ func (d *DumpJob) buildLogicalDumpCommand() []string {
418503
// Define if restore directly or export to dump location.
419504
if d.DumpOptions.Restore != nil {
420505
dumpCmd = append(dumpCmd, "--format", customFormat)
421-
dumpCmd = append(dumpCmd, d.buildLogicalRestoreCommand()...)
506+
dumpCmd = append(dumpCmd, d.buildLogicalRestoreCommand(d.DumpOptions.Source.Connection.DBName)...)
422507
cmd := strings.Join(dumpCmd, " ")
423508

424509
log.Dbg(cmd)
@@ -431,10 +516,15 @@ func (d *DumpJob) buildLogicalDumpCommand() []string {
431516
return dumpCmd
432517
}
433518

434-
func (d *DumpJob) buildLogicalRestoreCommand() []string {
435-
restoreCmd := []string{"|", "pg_restore", "--username", d.globalCfg.Database.User(), "--create", "--dbname", defaults.DBName,
519+
func (d *DumpJob) buildLogicalRestoreCommand(dbName string) []string {
520+
restoreCmd := []string{"|", "pg_restore", "--username", d.globalCfg.Database.User(), "--dbname", defaults.DBName,
436521
"--no-privileges", "--no-owner"}
437522

523+
if dbName != defaults.DBName {
524+
// To avoid recreating of the default database.
525+
restoreCmd = append(restoreCmd, "--create")
526+
}
527+
438528
if d.Restore.ForceInit {
439529
restoreCmd = append(restoreCmd, "--clean", "--if-exists")
440530
}

pkg/retrieval/engine/postgres/logical/restore.go

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/docker/docker/api/types"
1717
"github.com/docker/docker/api/types/container"
18+
"github.com/docker/docker/api/types/mount"
1819
"github.com/docker/docker/api/types/network"
1920
"github.com/docker/docker/client"
2021
"github.com/pkg/errors"
@@ -56,12 +57,13 @@ type RestoreJob struct {
5657

5758
// RestoreOptions defines a logical restore options.
5859
type RestoreOptions struct {
59-
DumpLocation string `yaml:"dumpLocation"`
60-
DockerImage string `yaml:"dockerImage"`
61-
DBName string `yaml:"dbname"`
62-
ForceInit bool `yaml:"forceInit"`
63-
ParallelJobs int `yaml:"parallelJobs"`
64-
Partial Partial `yaml:"partial"`
60+
DumpLocation string `yaml:"dumpLocation"`
61+
DockerImage string `yaml:"dockerImage"`
62+
DBName string `yaml:"dbname"`
63+
ForceInit bool `yaml:"forceInit"`
64+
ParallelJobs int `yaml:"parallelJobs"`
65+
Partial Partial `yaml:"partial"`
66+
Configs map[string]string `yaml:"configs"`
6567
}
6668

6769
// Partial defines tables and rules for a partial logical restore.
@@ -172,13 +174,28 @@ func (r *RestoreJob) Run(ctx context.Context) (err error) {
172174
return errors.Wrapf(err, "failed to start container %q", r.restoreContainerName())
173175
}
174176

177+
dataDir := r.fsPool.DataDir()
178+
175179
log.Msg("Waiting for container readiness")
176180

177181
if err := tools.CheckContainerReadiness(ctx, r.dockerClient, restoreCont.ID); err != nil {
178-
return errors.Wrap(err, "failed to readiness check")
182+
var errHealthCheck *tools.ErrHealthCheck
183+
if !errors.As(err, &errHealthCheck) {
184+
return errors.Wrap(err, "failed to readiness check")
185+
}
186+
187+
if err := setupPGData(ctx, r.dockerClient, dataDir, restoreCont.ID); err != nil {
188+
return errors.Wrap(err, "failed to set up Postgres data")
189+
}
179190
}
180191

181-
restoreCommand := r.buildLogicalRestoreCommand()
192+
if len(r.RestoreOptions.Configs) > 0 {
193+
if err := updateConfigs(ctx, r.dockerClient, dataDir, restoreCont.ID, r.RestoreOptions.Configs); err != nil {
194+
return errors.Wrap(err, "failed to update configs")
195+
}
196+
}
197+
198+
restoreCommand := r.buildLogicalRestoreCommand(r.DBName)
182199
log.Msg("Running restore command: ", restoreCommand)
183200

184201
if len(r.Partial.Tables) > 0 {
@@ -231,6 +248,12 @@ func (r *RestoreJob) buildHostConfig(ctx context.Context) (*container.HostConfig
231248
return nil, err
232249
}
233250

251+
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
252+
Type: mount.TypeBind,
253+
Source: r.RestoreOptions.DumpLocation,
254+
Target: r.RestoreOptions.DumpLocation,
255+
})
256+
234257
return hostConfig, nil
235258
}
236259

@@ -297,10 +320,15 @@ func (r *RestoreJob) updateDataStateAt() {
297320
r.fsPool.SetDSA(dsaTime)
298321
}
299322

300-
func (r *RestoreJob) buildLogicalRestoreCommand() []string {
301-
restoreCmd := []string{"pg_restore", "--username", r.globalCfg.Database.User(), "--dbname", defaults.DBName, "--create",
323+
func (r *RestoreJob) buildLogicalRestoreCommand(dbName string) []string {
324+
restoreCmd := []string{"pg_restore", "--username", r.globalCfg.Database.User(), "--dbname", defaults.DBName,
302325
"--no-privileges", "--no-owner"}
303326

327+
if dbName != defaults.DBName {
328+
// To avoid recreating of the default database.
329+
restoreCmd = append(restoreCmd, "--create")
330+
}
331+
304332
if r.ForceInit {
305333
restoreCmd = append(restoreCmd, "--clean", "--if-exists")
306334
}

pkg/retrieval/engine/postgres/logical/restore_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ func TestRestoreCommandBuilding(t *testing.T) {
3333
ForceInit: false,
3434
DumpLocation: "/tmp/db.dump",
3535
},
36-
Command: []string{"pg_restore", "--username", "john", "--dbname", "postgres", "--create", "--no-privileges", "--no-owner", "--jobs", "1", "/tmp/db.dump"},
36+
Command: []string{"pg_restore", "--username", "john", "--dbname", "postgres", "--no-privileges", "--no-owner", "--create", "--jobs", "1", "/tmp/db.dump"},
3737
},
3838
{
3939
CopyOptions: RestoreOptions{
4040
ParallelJobs: 4,
4141
ForceInit: true,
4242
},
43-
Command: []string{"pg_restore", "--username", "john", "--dbname", "postgres", "--create", "--no-privileges", "--no-owner", "--clean", "--if-exists", "--jobs", "4", ""},
43+
Command: []string{"pg_restore", "--username", "john", "--dbname", "postgres", "--no-privileges", "--no-owner", "--create", "--clean", "--if-exists", "--jobs", "4", ""},
4444
},
4545
{
4646
CopyOptions: RestoreOptions{
@@ -49,13 +49,13 @@ func TestRestoreCommandBuilding(t *testing.T) {
4949
Partial: Partial{Tables: []string{"test", "users"}},
5050
DumpLocation: "/tmp/db.dump",
5151
},
52-
Command: []string{"pg_restore", "--username", "john", "--dbname", "postgres", "--create", "--no-privileges", "--no-owner", "--jobs", "1", "--table", "test", "--table", "users", "/tmp/db.dump"},
52+
Command: []string{"pg_restore", "--username", "john", "--dbname", "postgres", "--no-privileges", "--no-owner", "--create", "--jobs", "1", "--table", "test", "--table", "users", "/tmp/db.dump"},
5353
},
5454
}
5555

5656
for _, tc := range testCases {
5757
logicalJob.RestoreOptions = tc.CopyOptions
58-
restoreCommand := logicalJob.buildLogicalRestoreCommand()
58+
restoreCommand := logicalJob.buildLogicalRestoreCommand(tc.CopyOptions.DBName)
5959

6060
assert.Equal(t, restoreCommand, tc.Command)
6161
}

0 commit comments

Comments
 (0)