Skip to content

Commit 5d82eb0

Browse files
snapshot: add pg_dump command builder with 'snapshot databases' (#889)
Add simple utility for generating commands that can then be run/modified to create database dumps. Right now, it's mostly for reference - as we build experience with different setups we can see if this can be made more flexible in the right ways. I believe a templating command like this is important to have, however, since we want to make sure all databases are exported and the correct flags are used with pg_dump, and documentation + manual copy-paste is error-prone. Co-authored-by: Michael Lin <[email protected]>
1 parent 4d2957c commit 5d82eb0

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

cmd/src/snapshot_databases.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"strings"
8+
9+
"github.com/sourcegraph/sourcegraph/lib/errors"
10+
"github.com/sourcegraph/sourcegraph/lib/output"
11+
"gopkg.in/yaml.v3"
12+
13+
"github.com/sourcegraph/src-cli/internal/pgdump"
14+
)
15+
16+
func init() {
17+
usage := `'src snapshot databases' generates commands to export Sourcegraph database dumps.
18+
Note that these commands are intended for use as reference - you may need to adjust the commands for your deployment.
19+
20+
USAGE
21+
src [-v] snapshot databases <pg_dump|docker|kubectl> [--targets=<docker|k8s|"targets.yaml">]
22+
23+
TARGETS FILES
24+
Predefined targets are available based on default Sourcegraph configurations ('docker', 'k8s').
25+
Custom targets configuration can be provided in YAML format with '--targets=target.yaml', e.g.
26+
27+
primary:
28+
target: ... # the DSN of the database deployment, e.g. in docker, the name of the database container
29+
dbname: ... # name of database
30+
username: ... # username for database access
31+
password: ... # password for database access - only include password if it is non-sensitive
32+
codeintel:
33+
# same as above
34+
codeinsights:
35+
# same as above
36+
37+
See the pgdump.Targets type for more details.
38+
`
39+
flagSet := flag.NewFlagSet("databases", flag.ExitOnError)
40+
targetsKeyFlag := flagSet.String("targets", "auto", "predefined targets ('docker' or 'k8s'), or a custom targets.yaml file")
41+
42+
snapshotCommands = append(snapshotCommands, &command{
43+
flagSet: flagSet,
44+
handler: func(args []string) error {
45+
if err := flagSet.Parse(args); err != nil {
46+
return err
47+
}
48+
out := output.NewOutput(flagSet.Output(), output.OutputOpts{Verbose: *verbose})
49+
50+
var builder string
51+
if len(args) > 0 {
52+
builder = args[0]
53+
}
54+
55+
targetKey := "docker"
56+
var commandBuilder pgdump.CommandBuilder
57+
switch builder {
58+
case "pg_dump", "":
59+
targetKey = "local"
60+
commandBuilder = func(t pgdump.Target) (string, error) {
61+
cmd := pgdump.Command(t)
62+
if t.Target != "" {
63+
return fmt.Sprintf("%s --host=%s", cmd, t.Target), nil
64+
}
65+
return cmd, nil
66+
}
67+
case "docker":
68+
commandBuilder = func(t pgdump.Target) (string, error) {
69+
return fmt.Sprintf("docker exec -it %s sh -c '%s'", t.Target, pgdump.Command(t)), nil
70+
}
71+
case "kubectl":
72+
targetKey = "k8s"
73+
commandBuilder = func(t pgdump.Target) (string, error) {
74+
return fmt.Sprintf("kubectl exec -it %s -- bash -c '%s'", t.Target, pgdump.Command(t)), nil
75+
}
76+
default:
77+
return errors.Newf("unknown or invalid template type %q", builder)
78+
}
79+
if *targetsKeyFlag != "auto" {
80+
targetKey = *targetsKeyFlag
81+
}
82+
83+
targets, ok := predefinedDatabaseDumpTargets[targetKey]
84+
if !ok {
85+
out.WriteLine(output.Emojif(output.EmojiInfo, "Using targets defined in targets file %q", targetKey))
86+
f, err := os.Open(targetKey)
87+
if err != nil {
88+
return errors.Wrapf(err, "invalid targets file %q", targetKey)
89+
}
90+
if err := yaml.NewDecoder(f).Decode(&targets); err != nil {
91+
return errors.Wrapf(err, "invalid targets file %q", targetKey)
92+
}
93+
} else {
94+
out.WriteLine(output.Emojif(output.EmojiInfo, "Using predefined targets for %s environments", targetKey))
95+
}
96+
97+
commands, err := pgdump.BuildCommands(commandBuilder, targets)
98+
if err != nil {
99+
return errors.Wrap(err, "failed to build commands")
100+
}
101+
102+
b := out.Block(output.Emoji(output.EmojiSuccess, "Commands generated - run them all to generate required database dumps:"))
103+
b.Write("\n" + strings.Join(commands, "\n"))
104+
b.Close()
105+
106+
out.WriteLine(output.Styledf(output.StyleSuggestion, "Note that you may need to do some additional setup, such as authentication, beforehand."))
107+
108+
return nil
109+
},
110+
usageFunc: func() { fmt.Fprint(flag.CommandLine.Output(), usage) },
111+
})
112+
}
113+
114+
// predefinedDatabaseDumpTargets is based on default Sourcegraph configurations.
115+
var predefinedDatabaseDumpTargets = map[string]pgdump.Targets{
116+
"local": {
117+
Primary: pgdump.Target{
118+
DBName: "sg",
119+
Username: "sg",
120+
Password: "sg",
121+
},
122+
CodeIntel: pgdump.Target{
123+
DBName: "sg",
124+
Username: "sg",
125+
Password: "sg",
126+
},
127+
CodeInsights: pgdump.Target{
128+
DBName: "postgres",
129+
Username: "postgres",
130+
Password: "password",
131+
},
132+
},
133+
"docker": { // based on deploy-sourcegraph-managed
134+
Primary: pgdump.Target{
135+
Target: "pgsql",
136+
DBName: "sg",
137+
Username: "sg",
138+
Password: "sg",
139+
},
140+
CodeIntel: pgdump.Target{
141+
Target: "codeintel-db",
142+
DBName: "sg",
143+
Username: "sg",
144+
Password: "sg",
145+
},
146+
CodeInsights: pgdump.Target{
147+
Target: "codeinsights-db",
148+
DBName: "postgres",
149+
Username: "postgres",
150+
Password: "password",
151+
},
152+
},
153+
"k8s": { // based on deploy-sourcegraph-helm
154+
Primary: pgdump.Target{
155+
Target: "statefulset/pgsql",
156+
DBName: "sg",
157+
Username: "sg",
158+
Password: "sg",
159+
},
160+
CodeIntel: pgdump.Target{
161+
Target: "statefulset/codeintel-db",
162+
DBName: "sg",
163+
Username: "sg",
164+
Password: "sg",
165+
},
166+
CodeInsights: pgdump.Target{
167+
Target: "statefulset/codeinsights-db",
168+
DBName: "postgres",
169+
Username: "postgres",
170+
Password: "password",
171+
},
172+
},
173+
}

internal/pgdump/pgdump.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package pgdump
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/sourcegraph/sourcegraph/lib/errors"
7+
)
8+
9+
// Targets represents configuration for each of Sourcegraph's databases.
10+
type Targets struct {
11+
Primary Target `yaml:"primary"`
12+
CodeIntel Target `yaml:"codeintel"`
13+
CodeInsights Target `yaml:"codeinsights"`
14+
}
15+
16+
// Target represents a database for pg_dump to export.
17+
type Target struct {
18+
// Target is the DSN of the database deployment:
19+
//
20+
// - in docker, the name of the database container, e.g. pgsql, codeintel-db, codeinsights-db
21+
// - in k8s, the name of the deployment or statefulset, e.g. deploy/pgsql, sts/pgsql
22+
// - in plain pg_dump, the server host or socket directory
23+
Target string `yaml:"target"`
24+
25+
DBName string `yaml:"dbname"`
26+
Username string `yaml:"username"`
27+
28+
// Only include password if non-sensitive
29+
Password string `yaml:"password"`
30+
}
31+
32+
// Command generates a pg_dump command that can be used for on-prem-to-Cloud migrations.
33+
func Command(t Target) string {
34+
dump := fmt.Sprintf("pg_dump --no-owner --format=p --no-acl --username=%s --dbname=%s",
35+
t.Username, t.DBName)
36+
if t.Password == "" {
37+
return dump
38+
}
39+
return fmt.Sprintf("PGPASSWORD=%s %s", t.Password, dump)
40+
}
41+
42+
type CommandBuilder func(Target) (string, error)
43+
44+
// BuildCommands generates commands that output Postgres dumps and sends them to predefined
45+
// files for each target database.
46+
func BuildCommands(commandBuilder CommandBuilder, targets Targets) ([]string, error) {
47+
var commands []string
48+
for _, t := range []struct {
49+
Output string
50+
Target Target
51+
}{{
52+
Output: "primary.sql",
53+
Target: targets.Primary,
54+
}, {
55+
Output: "codeintel.sql",
56+
Target: targets.CodeIntel,
57+
}, {
58+
Output: "codeinsights.sql",
59+
Target: targets.CodeInsights,
60+
}} {
61+
c, err := commandBuilder(t.Target)
62+
if err != nil {
63+
return nil, errors.Wrapf(err, "generating command for %q", t.Output)
64+
}
65+
commands = append(commands, fmt.Sprintf("%s > %s", c, t.Output))
66+
}
67+
return commands, nil
68+
}

0 commit comments

Comments
 (0)