Skip to content

Commit 805dcd0

Browse files
authored
feat: support restoring local db from backup (#3015)
1 parent 94cf287 commit 805dcd0

File tree

7 files changed

+325
-247
lines changed

7 files changed

+325
-247
lines changed

cmd/db.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,13 @@ var (
219219
},
220220
}
221221

222+
fromBackup string
223+
222224
dbStartCmd = &cobra.Command{
223225
Use: "start",
224226
Short: "Starts local Postgres database",
225227
RunE: func(cmd *cobra.Command, args []string) error {
226-
return start.Run(cmd.Context(), afero.NewOsFs())
228+
return start.Run(cmd.Context(), fromBackup, afero.NewOsFs())
227229
},
228230
}
229231

@@ -329,6 +331,8 @@ func init() {
329331
lintFlags.Var(&lintFailOn, "fail-on", "Error level to exit with non-zero status.")
330332
dbCmd.AddCommand(dbLintCmd)
331333
// Build start command
334+
startFlags := dbStartCmd.Flags()
335+
startFlags.StringVar(&fromBackup, "from-backup", "", "Path to a logical backup file.")
332336
dbCmd.AddCommand(dbStartCmd)
333337
// Build test command
334338
dbCmd.AddCommand(dbTestCmd)

internal/db/start/start.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"os"
9+
"path/filepath"
910
"strconv"
1011
"strings"
1112
"time"
@@ -29,11 +30,15 @@ var (
2930
HealthTimeout = 120 * time.Second
3031
//go:embed templates/schema.sql
3132
initialSchema string
33+
//go:embed templates/webhook.sql
34+
webhookSchema string
3235
//go:embed templates/_supabase.sql
3336
_supabaseSchema string
37+
//go:embed templates/restore.sh
38+
restoreScript string
3439
)
3540

36-
func Run(ctx context.Context, fsys afero.Fs) error {
41+
func Run(ctx context.Context, fromBackup string, fsys afero.Fs) error {
3742
if err := utils.LoadConfigFS(fsys); err != nil {
3843
return err
3944
}
@@ -43,7 +48,7 @@ func Run(ctx context.Context, fsys afero.Fs) error {
4348
} else if !errors.Is(err, utils.ErrNotRunning) {
4449
return err
4550
}
46-
err := StartDatabase(ctx, fsys, os.Stderr)
51+
err := StartDatabase(ctx, fromBackup, fsys, os.Stderr)
4752
if err != nil {
4853
if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
4954
fmt.Fprintln(os.Stderr, err)
@@ -86,6 +91,7 @@ cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
8691
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
8792
docker-entrypoint.sh postgres -D /etc/postgresql
8893
` + initialSchema + `
94+
` + webhookSchema + `
8995
` + _supabaseSchema + `
9096
EOF
9197
` + utils.Config.Db.RootKey + `
@@ -116,7 +122,7 @@ func NewHostConfig() container.HostConfig {
116122
return hostConfig
117123
}
118124

119-
func StartDatabase(ctx context.Context, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
125+
func StartDatabase(ctx context.Context, fromBackup string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
120126
config := NewContainerConfig()
121127
hostConfig := NewHostConfig()
122128
networkingConfig := network.NetworkingConfig{
@@ -137,11 +143,35 @@ EOF
137143
EOF`}
138144
hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
139145
}
146+
if len(fromBackup) > 0 {
147+
config.Entrypoint = []string{"sh", "-c", `
148+
cat <<'EOF' > /etc/postgresql.schema.sql && \
149+
cat <<'EOF' > /docker-entrypoint-initdb.d/migrate.sh && \
150+
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
151+
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
152+
docker-entrypoint.sh postgres -D /etc/postgresql
153+
` + initialSchema + `
154+
` + _supabaseSchema + `
155+
EOF
156+
` + restoreScript + `
157+
EOF
158+
` + utils.Config.Db.RootKey + `
159+
EOF
160+
` + utils.Config.Db.Settings.ToPostgresConfig() + `
161+
EOF`}
162+
if !filepath.IsAbs(fromBackup) {
163+
fromBackup = filepath.Join(utils.CurrentDirAbs, fromBackup)
164+
}
165+
hostConfig.Binds = append(hostConfig.Binds, utils.ToDockerPath(fromBackup)+":/etc/backup.sql:ro")
166+
}
140167
// Creating volume will not override existing volume, so we must inspect explicitly
141168
_, err := utils.Docker.VolumeInspect(ctx, utils.DbId)
142169
utils.NoBackupVolume = client.IsErrNotFound(err)
143170
if utils.NoBackupVolume {
144171
fmt.Fprintln(w, "Starting database...")
172+
} else if len(fromBackup) > 0 {
173+
utils.CmdSuggestion = fmt.Sprintf("Run %s to remove existing docker volumes.", utils.Aqua("supabase stop --no-backup"))
174+
return errors.Errorf("backup volume already exists")
145175
} else {
146176
fmt.Fprintln(w, "Starting database from backup...")
147177
}
@@ -152,7 +182,11 @@ EOF`}
152182
return err
153183
}
154184
// Initialize if we are on PG14 and there's no existing db volume
155-
if utils.NoBackupVolume {
185+
if len(fromBackup) > 0 {
186+
if err := initSchema15(ctx, utils.DbId); err != nil {
187+
return err
188+
}
189+
} else if utils.NoBackupVolume {
156190
if err := SetupLocalDatabase(ctx, "", fsys, w, options...); err != nil {
157191
return err
158192
}

internal/db/start/start_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func TestStartDatabase(t *testing.T) {
8989
conn.Query(roles).
9090
Reply("CREATE ROLE")
9191
// Run test
92-
err := StartDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
92+
err := StartDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept)
9393
// Check error
9494
assert.NoError(t, err)
9595
assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -124,7 +124,7 @@ func TestStartDatabase(t *testing.T) {
124124
},
125125
}})
126126
// Run test
127-
err := StartDatabase(context.Background(), fsys, io.Discard)
127+
err := StartDatabase(context.Background(), "", fsys, io.Discard)
128128
// Check error
129129
assert.NoError(t, err)
130130
assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -149,7 +149,7 @@ func TestStartDatabase(t *testing.T) {
149149
Get("/v" + utils.Docker.ClientVersion() + "/images/" + utils.GetRegistryImageUrl(utils.Config.Db.Image) + "/json").
150150
Reply(http.StatusInternalServerError)
151151
// Run test
152-
err := StartDatabase(context.Background(), fsys, io.Discard)
152+
err := StartDatabase(context.Background(), "", fsys, io.Discard)
153153
// Check error
154154
assert.ErrorContains(t, err, "request returned Internal Server Error for API route and version")
155155
assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -161,7 +161,7 @@ func TestStartCommand(t *testing.T) {
161161
// Setup in-memory fs
162162
fsys := afero.NewMemMapFs()
163163
// Run test
164-
err := Run(context.Background(), fsys)
164+
err := Run(context.Background(), "", fsys)
165165
// Check error
166166
assert.ErrorIs(t, err, os.ErrNotExist)
167167
})
@@ -177,7 +177,7 @@ func TestStartCommand(t *testing.T) {
177177
Get("/v" + utils.Docker.ClientVersion() + "/containers").
178178
ReplyError(errors.New("network error"))
179179
// Run test
180-
err := Run(context.Background(), fsys)
180+
err := Run(context.Background(), "", fsys)
181181
// Check error
182182
assert.ErrorContains(t, err, "network error")
183183
assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -195,7 +195,7 @@ func TestStartCommand(t *testing.T) {
195195
Reply(http.StatusOK).
196196
JSON(types.ContainerJSON{})
197197
// Run test
198-
err := Run(context.Background(), fsys)
198+
err := Run(context.Background(), "", fsys)
199199
// Check error
200200
assert.NoError(t, err)
201201
assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -221,7 +221,7 @@ func TestStartCommand(t *testing.T) {
221221
// Cleanup resources
222222
apitest.MockDockerStop(utils.Docker)
223223
// Run test
224-
err := Run(context.Background(), fsys)
224+
err := Run(context.Background(), "", fsys)
225225
// Check error
226226
assert.ErrorContains(t, err, "network error")
227227
assert.Empty(t, apitest.ListUnmatchedRequests())
@@ -350,7 +350,7 @@ func TestStartDatabaseWithCustomSettings(t *testing.T) {
350350
defer conn.Close(t)
351351

352352
// Run test
353-
err := StartDatabase(context.Background(), fsys, io.Discard, conn.Intercept)
353+
err := StartDatabase(context.Background(), "", fsys, io.Discard, conn.Intercept)
354354

355355
// Check error
356356
assert.NoError(t, err)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
#######################################
5+
# Used by both ami and docker builds to initialise database schema.
6+
# Env vars:
7+
# POSTGRES_DB defaults to postgres
8+
# POSTGRES_HOST defaults to localhost
9+
# POSTGRES_PORT defaults to 5432
10+
# POSTGRES_PASSWORD defaults to ""
11+
# USE_DBMATE defaults to ""
12+
# Exit code:
13+
# 0 if migration succeeds, non-zero on error.
14+
#######################################
15+
16+
export PGDATABASE="${POSTGRES_DB:-postgres}"
17+
export PGHOST="${POSTGRES_HOST:-localhost}"
18+
export PGPORT="${POSTGRES_PORT:-5432}"
19+
export PGPASSWORD="${POSTGRES_PASSWORD:-}"
20+
21+
echo "$0: restoring roles"
22+
cat "/etc/backup.sql" \
23+
| grep 'CREATE ROLE' \
24+
| grep -v 'supabase_admin' \
25+
| psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin
26+
27+
echo "$0: restoring schema"
28+
cat "/etc/backup.sql" \
29+
| sed -E 's/^CREATE VIEW /CREATE OR REPLACE VIEW /' \
30+
| sed -E 's/^CREATE FUNCTION /CREATE OR REPLACE FUNCTION /' \
31+
| sed -E 's/^CREATE TRIGGER /CREATE OR REPLACE TRIGGER /' \
32+
| sed -E 's/^GRANT ALL ON FUNCTION graphql_public\./-- &/' \
33+
| sed -E 's/^CREATE ROLE /-- &/' \
34+
| psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin
35+
36+
# run any post migration script to update role passwords
37+
postinit="/etc/postgresql.schema.sql"
38+
if [ -e "$postinit" ]; then
39+
echo "$0: running $postinit"
40+
psql -v ON_ERROR_STOP=1 --no-password --no-psqlrc -U supabase_admin -f "$postinit"
41+
fi

0 commit comments

Comments
 (0)