/
postgres.go
106 lines (92 loc) · 2.88 KB
/
postgres.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package snapshot
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
)
// Postgres creates snapshots for Postgres containers. It dumps the
// database using pg_dumpall.
type Postgres struct {
client *client.Client
dbUser string
}
// NewPostgres creates a new Postgres snapshotter.
func NewPostgres(c *client.Client, dbUser string) Snapshotter {
return &Postgres{c, dbUser}
}
// Create creates a new snapshot.
func (c *Postgres) Create(ctx context.Context, container types.ContainerJSON, title, imageName string) error {
buildContext, err := ioutil.TempDir("", "dksnap-context")
if err != nil {
return fmt.Errorf("make build context dir: %w", err)
}
defer os.RemoveAll(buildContext)
dump, err := exec(ctx, c.client, container.ID, []string{"pg_dumpall", "-U", c.dbUser})
if err != nil {
return fmt.Errorf("dump: %w", err)
}
if err := ioutil.WriteFile(filepath.Join(buildContext, "dump.sql"), dump, 0644); err != nil {
return fmt.Errorf("write dump: %w", err)
}
// Load the dump from a script so that errors are ignored.
// Errors are expected with the current dump file if the dump contains the
// same user as the POSTGRES_USER environment variable.
loadScript := []byte(`#!/bin/bash
psql --username "${POSTGRES_USER:-postgres}" -d "${POSTGRES_DB:-postgres}" --no-password < /dksnap-dump.sql`)
if err := ioutil.WriteFile(filepath.Join(buildContext, "load-dump.sh"), loadScript, 0755); err != nil {
return fmt.Errorf("write dump: %w", err)
}
err = buildImage(ctx, c.client, buildOptions{
baseImage: container.Image,
context: buildContext,
bootCommands: []string{
"rm -rf /var/lib/postgresql/data/*",
},
buildInstructions: []string{
"COPY load-dump.sh /docker-entrypoint-initdb.d/load-dump.sh",
"COPY dump.sql /dksnap-dump.sql",
},
title: title,
imageNames: []string{imageName},
dumpPath: "/dksnap-dump.sql",
})
if err != nil {
return fmt.Errorf("build image: %w", err)
}
return nil
}
func exec(ctx context.Context, dockerClient *client.Client, container string, cmd []string) ([]byte, error) {
execID, err := dockerClient.ContainerExecCreate(ctx, container, types.ExecConfig{
Cmd: cmd,
AttachStderr: true,
AttachStdout: true,
})
if err != nil {
return nil, err
}
execStream, err := dockerClient.ContainerExecAttach(ctx, execID.ID, types.ExecStartCheck{})
if err != nil {
return nil, err
}
defer execStream.Close()
var stdout, stderr bytes.Buffer
_, err = stdcopy.StdCopy(&stdout, &stderr, execStream.Reader)
if err != nil {
return nil, err
}
execStatus, err := dockerClient.ContainerExecInspect(ctx, execID.ID)
if err != nil {
return nil, err
}
if execStatus.ExitCode != 0 {
return nil, fmt.Errorf("non-zero exit %d: %s",
execStatus.ExitCode, stderr.String())
}
return stdout.Bytes(), nil
}