• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / cli / 18397610522

10 Oct 2025 05:42AM UTC coverage: 54.676% (-0.02%) from 54.696%
18397610522

Pull #4280

github

web-flow
Merge 234aef95f into 677e5c2bd
Pull Request #4280: fix: restore logical backup on pg 15.14

2 of 3 new or added lines in 1 file covered. (66.67%)

5 existing lines in 1 file now uncovered.

6408 of 11720 relevant lines covered (54.68%)

6.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.68
/internal/db/start/start.go
1
package start
2

3
import (
4
        "context"
5
        _ "embed"
6
        "fmt"
7
        "io"
8
        "os"
9
        "path/filepath"
10
        "strconv"
11
        "strings"
12
        "time"
13

14
        "github.com/cenkalti/backoff/v4"
15
        "github.com/containerd/errdefs"
16
        "github.com/docker/docker/api/types/container"
17
        "github.com/docker/docker/api/types/network"
18
        "github.com/docker/go-connections/nat"
19
        "github.com/go-errors/errors"
20
        "github.com/jackc/pgconn"
21
        "github.com/jackc/pgx/v4"
22
        "github.com/spf13/afero"
23
        "github.com/supabase/cli/internal/migration/apply"
24
        "github.com/supabase/cli/internal/status"
25
        "github.com/supabase/cli/internal/utils"
26
        "github.com/supabase/cli/internal/utils/flags"
27
        "github.com/supabase/cli/pkg/config"
28
        "github.com/supabase/cli/pkg/migration"
29
        "github.com/supabase/cli/pkg/vault"
30
)
31

32
var (
33
        HealthTimeout = 120 * time.Second
34
        //go:embed templates/schema.sql
35
        initialSchema string
36
        //go:embed templates/webhook.sql
37
        webhookSchema string
38
        //go:embed templates/_supabase.sql
39
        _supabaseSchema string
40
        //go:embed templates/restore.sh
41
        restoreScript string
42
)
43

44
func Run(ctx context.Context, fromBackup string, fsys afero.Fs) error {
4✔
45
        if err := flags.LoadConfig(fsys); err != nil {
5✔
46
                return err
1✔
47
        }
1✔
48
        if err := utils.AssertSupabaseDbIsRunning(); err == nil {
4✔
49
                fmt.Fprintln(os.Stderr, "Postgres database is already running.")
1✔
50
                return nil
1✔
51
        } else if !errors.Is(err, utils.ErrNotRunning) {
4✔
52
                return err
1✔
53
        }
1✔
54
        err := StartDatabase(ctx, fromBackup, fsys, os.Stderr)
1✔
55
        if err != nil {
2✔
56
                if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
1✔
57
                        fmt.Fprintln(os.Stderr, err)
×
58
                }
×
59
        }
60
        return err
1✔
61
}
62

63
func NewContainerConfig(args ...string) container.Config {
23✔
64
        if utils.Config.Db.MajorVersion >= 14 {
44✔
65
                // Extensions schema does not exist on PG13 and below
21✔
66
                args = append(args, "-c", "search_path='$user,public,extensions'")
21✔
67
        }
21✔
68
        env := []string{
23✔
69
                "POSTGRES_PASSWORD=" + utils.Config.Db.Password,
23✔
70
                "POSTGRES_HOST=/var/run/postgresql",
23✔
71
                "JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
23✔
72
                fmt.Sprintf("JWT_EXP=%d", utils.Config.Auth.JwtExpiry),
23✔
73
        }
23✔
74
        if len(utils.Config.Experimental.OrioleDBVersion) > 0 {
23✔
75
                env = append(env,
×
76
                        "POSTGRES_INITDB_ARGS=--lc-collate=C --lc-ctype=C",
×
77
                        fmt.Sprintf("S3_ENABLED=%t", true),
×
78
                        "S3_HOST="+utils.Config.Experimental.S3Host,
×
79
                        "S3_REGION="+utils.Config.Experimental.S3Region,
×
80
                        "S3_ACCESS_KEY="+utils.Config.Experimental.S3AccessKey,
×
81
                        "S3_SECRET_KEY="+utils.Config.Experimental.S3SecretKey,
×
82
                )
×
83
        } else if i := strings.IndexByte(utils.Config.Db.Image, ':'); config.VersionCompare(utils.Config.Db.Image[i+1:], "15.8.1.005") < 0 {
23✔
84
                env = append(env, "POSTGRES_INITDB_ARGS=--lc-collate=C.UTF-8")
×
85
        }
×
86
        config := container.Config{
23✔
87
                Image: utils.Config.Db.Image,
23✔
88
                Env:   env,
23✔
89
                Healthcheck: &container.HealthConfig{
23✔
90
                        Test:     []string{"CMD", "pg_isready", "-U", "postgres", "-h", "127.0.0.1", "-p", "5432"},
23✔
91
                        Interval: 10 * time.Second,
23✔
92
                        Timeout:  2 * time.Second,
23✔
93
                        Retries:  3,
23✔
94
                },
23✔
95
                Entrypoint: []string{"sh", "-c", `
23✔
96
cat <<'EOF' > /etc/postgresql.schema.sql && \
23✔
97
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
23✔
98
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
23✔
99
docker-entrypoint.sh postgres -D /etc/postgresql ` + strings.Join(args, " ") + `
23✔
100
` + initialSchema + `
23✔
101
` + webhookSchema + `
23✔
102
` + _supabaseSchema + `
23✔
103
EOF
23✔
104
` + utils.Config.Db.RootKey.Value + `
23✔
105
EOF
23✔
106
` + utils.Config.Db.Settings.ToPostgresConfig() + `
23✔
107
EOF`},
23✔
108
        }
23✔
109
        if utils.Config.Db.MajorVersion <= 14 {
30✔
110
                config.Entrypoint = []string{"sh", "-c", `
7✔
111
cat <<'EOF' > /docker-entrypoint-initdb.d/supabase_schema.sql && \
7✔
112
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
7✔
113
docker-entrypoint.sh postgres -D /etc/postgresql ` + strings.Join(args, " ") + `
7✔
114
` + _supabaseSchema + `
7✔
115
EOF
7✔
116
` + utils.Config.Db.Settings.ToPostgresConfig() + `
7✔
117
EOF`}
7✔
118
        }
7✔
119
        return config
23✔
120
}
121

122
func NewHostConfig() container.HostConfig {
8✔
123
        hostPort := strconv.FormatUint(uint64(utils.Config.Db.Port), 10)
8✔
124
        hostConfig := container.HostConfig{
8✔
125
                PortBindings:  nat.PortMap{"5432/tcp": []nat.PortBinding{{HostPort: hostPort}}},
8✔
126
                RestartPolicy: container.RestartPolicy{Name: "always"},
8✔
127
                Binds: []string{
8✔
128
                        utils.DbId + ":/var/lib/postgresql/data",
8✔
129
                        utils.ConfigId + ":/etc/postgresql-custom",
8✔
130
                },
8✔
131
        }
8✔
132
        if utils.Config.Db.MajorVersion <= 14 {
9✔
133
                hostConfig.Tmpfs = map[string]string{"/docker-entrypoint-initdb.d": ""}
1✔
134
        }
1✔
135
        return hostConfig
8✔
136
}
137

138
func StartDatabase(ctx context.Context, fromBackup string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
7✔
139
        config := NewContainerConfig()
7✔
140
        hostConfig := NewHostConfig()
7✔
141
        networkingConfig := network.NetworkingConfig{
7✔
142
                EndpointsConfig: map[string]*network.EndpointSettings{
7✔
143
                        utils.NetId: {
7✔
144
                                Aliases: utils.DbAliases,
7✔
145
                        },
7✔
146
                },
7✔
147
        }
7✔
148
        if len(fromBackup) > 0 {
7✔
149
                config.Entrypoint = []string{"sh", "-c", `
×
150
cat <<'EOF' > /etc/postgresql.schema.sql && \
×
151
cat <<'EOF' > /docker-entrypoint-initdb.d/migrate.sh && \
×
152
cat <<'EOF' > /etc/postgresql-custom/pgsodium_root.key && \
×
153
cat <<'EOF' >> /etc/postgresql/postgresql.conf && \
×
154
docker-entrypoint.sh postgres -D /etc/postgresql
×
155
` + initialSchema + `
×
156
` + _supabaseSchema + `
×
157
EOF
×
158
` + restoreScript + `
×
159
EOF
×
160
` + utils.Config.Db.RootKey.Value + `
×
161
EOF
×
162
` + utils.Config.Db.Settings.ToPostgresConfig() + `
×
NEW
163
cron.launch_active_jobs = off
×
164
EOF`}
×
165
                if !filepath.IsAbs(fromBackup) {
×
166
                        fromBackup = filepath.Join(utils.CurrentDirAbs, fromBackup)
×
167
                }
×
168
                hostConfig.Binds = append(hostConfig.Binds, utils.ToDockerPath(fromBackup)+":/etc/backup.sql:ro")
×
169
        }
170
        // Creating volume will not override existing volume, so we must inspect explicitly
171
        _, err := utils.Docker.VolumeInspect(ctx, utils.DbId)
7✔
172
        utils.NoBackupVolume = errdefs.IsNotFound(err)
7✔
173
        if utils.NoBackupVolume {
10✔
174
                fmt.Fprintln(w, "Starting database...")
3✔
175
        } else if len(fromBackup) > 0 {
7✔
176
                utils.CmdSuggestion = fmt.Sprintf("Run %s to remove existing docker volumes.", utils.Aqua("supabase stop --no-backup"))
×
177
                return errors.Errorf("backup volume already exists")
×
178
        } else {
4✔
179
                fmt.Fprintln(w, "Starting database from backup...")
4✔
180
        }
4✔
181
        if _, err := utils.DockerStart(ctx, config, hostConfig, networkingConfig, utils.DbId); err != nil {
9✔
182
                return err
2✔
183
        }
2✔
184
        // Ignore health check because restoring a large backup may take longer than 2 minutes
185
        if err := WaitForHealthyService(ctx, HealthTimeout, utils.DbId); err != nil && len(fromBackup) == 0 {
5✔
186
                return err
×
187
        }
×
188
        // Initialize if we are on PG14 and there's no existing db volume
189
        if utils.NoBackupVolume && len(fromBackup) == 0 {
8✔
190
                if err := SetupLocalDatabase(ctx, "", fsys, w, options...); err != nil {
3✔
191
                        return err
×
192
                }
×
193
        }
194
        return initCurrentBranch(fsys)
5✔
195
}
196

197
func NewBackoffPolicy(ctx context.Context, timeout time.Duration) backoff.BackOff {
36✔
198
        policy := backoff.WithMaxRetries(
36✔
199
                backoff.NewConstantBackOff(time.Second),
36✔
200
                uint64(timeout.Seconds()),
36✔
201
        )
36✔
202
        return backoff.WithContext(policy, ctx)
36✔
203
}
36✔
204

205
func WaitForHealthyService(ctx context.Context, timeout time.Duration, started ...string) error {
20✔
206
        probe := func() error {
40✔
207
                var errHealth []error
20✔
208
                var unhealthy []string
20✔
209
                for _, container := range started {
49✔
210
                        if err := status.IsServiceReady(ctx, container); err != nil {
32✔
211
                                unhealthy = append(unhealthy, container)
3✔
212
                                errHealth = append(errHealth, err)
3✔
213
                        }
3✔
214
                }
215
                started = unhealthy
20✔
216
                return errors.Join(errHealth...)
20✔
217
        }
218
        policy := NewBackoffPolicy(ctx, timeout)
20✔
219
        err := backoff.Retry(probe, policy)
20✔
220
        if err != nil && !errors.Is(err, context.Canceled) {
23✔
221
                // Print container logs for easier debugging
3✔
222
                for _, containerId := range started {
6✔
223
                        fmt.Fprintln(os.Stderr, containerId, "container logs:")
3✔
224
                        if err := utils.DockerStreamLogsOnce(context.Background(), containerId, os.Stderr, os.Stderr); err != nil {
6✔
225
                                fmt.Fprintln(os.Stderr, err)
3✔
226
                        }
3✔
227
                }
228
        }
229
        return err
20✔
230
}
231

232
func IsUnhealthyError(err error) bool {
×
233
        // Health check always returns a joinError
×
234
        _, ok := err.(interface{ Unwrap() []error })
×
235
        return ok
×
236
}
×
237

238
func initCurrentBranch(fsys afero.Fs) error {
8✔
239
        // Create _current_branch file to avoid breaking db branch commands
8✔
240
        if _, err := fsys.Stat(utils.CurrBranchPath); err == nil {
8✔
241
                return nil
×
242
        } else if !errors.Is(err, os.ErrNotExist) {
9✔
243
                return errors.Errorf("failed init current branch: %w", err)
1✔
244
        }
1✔
245
        return utils.WriteFile(utils.CurrBranchPath, []byte("main"), fsys)
7✔
246
}
247

248
func initSchema(ctx context.Context, conn *pgx.Conn, host string, w io.Writer) error {
15✔
249
        fmt.Fprintln(w, "Initialising schema...")
15✔
250
        if utils.Config.Db.MajorVersion <= 14 {
20✔
251
                if file, err := migration.NewMigrationFromReader(strings.NewReader(utils.GlobalsSql)); err != nil {
5✔
252
                        return err
×
253
                } else if err := file.ExecBatch(ctx, conn); err != nil {
7✔
254
                        return err
2✔
255
                }
2✔
256
                return InitSchema14(ctx, conn)
3✔
257
        }
258
        return initSchema15(ctx, host)
10✔
259
}
260

261
func InitSchema14(ctx context.Context, conn *pgx.Conn) error {
5✔
262
        sql := utils.InitialSchemaPg14Sql
5✔
263
        if utils.Config.Db.MajorVersion == 13 {
5✔
264
                sql = utils.InitialSchemaPg13Sql
×
265
        }
×
266
        file, err := migration.NewMigrationFromReader(strings.NewReader(sql))
5✔
267
        if err != nil {
5✔
268
                return err
×
269
        }
×
270
        return file.ExecBatch(ctx, conn)
5✔
271
}
272

273
func initRealtimeJob(host, jwks string) utils.DockerJob {
8✔
274
        return utils.DockerJob{
8✔
275
                Image: utils.Config.Realtime.Image,
8✔
276
                Env: []string{
8✔
277
                        "PORT=4000",
8✔
278
                        "DB_HOST=" + host,
8✔
279
                        "DB_PORT=5432",
8✔
280
                        "DB_USER=" + utils.SUPERUSER_ROLE,
8✔
281
                        "DB_PASSWORD=" + utils.Config.Db.Password,
8✔
282
                        "DB_NAME=postgres",
8✔
283
                        "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
8✔
284
                        "DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
8✔
285
                        fmt.Sprintf("API_JWT_JWKS=%s", jwks),
8✔
286
                        "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
287
                        "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
288
                        "APP_NAME=realtime",
8✔
289
                        "SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
8✔
290
                        "ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion),
8✔
291
                        "DNS_NODES=''",
8✔
292
                        "RLIMIT_NOFILE=",
8✔
293
                        "SEED_SELF_HOST=true",
8✔
294
                        "RUN_JANITOR=true",
8✔
295
                        fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength),
8✔
296
                },
8✔
297
                Cmd: []string{"/app/bin/realtime", "eval", fmt.Sprintf(`{:ok, _} = Application.ensure_all_started(:realtime)
8✔
298
{:ok, _} = Realtime.Tenants.health_check("%s")`, utils.Config.Realtime.TenantId)},
8✔
299
        }
8✔
300
}
8✔
301

302
func initStorageJob(host string) utils.DockerJob {
8✔
303
        return utils.DockerJob{
8✔
304
                Image: utils.Config.Storage.Image,
8✔
305
                Env: []string{
8✔
306
                        "DB_INSTALL_ROLES=false",
8✔
307
                        "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration,
8✔
308
                        "ANON_KEY=" + utils.Config.Auth.AnonKey.Value,
8✔
309
                        "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value,
8✔
310
                        "PGRST_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
311
                        fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
8✔
312
                        fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
8✔
313
                        "STORAGE_BACKEND=file",
8✔
314
                        "STORAGE_FILE_BACKEND_PATH=/mnt",
8✔
315
                        "TENANT_ID=stub",
8✔
316
                        // TODO: https://github.com/supabase/storage-api/issues/55
8✔
317
                        "REGION=stub",
8✔
318
                        "GLOBAL_S3_BUCKET=stub",
8✔
319
                },
8✔
320
                Cmd: []string{"node", "dist/scripts/migrate-call.js"},
8✔
321
        }
8✔
322
}
8✔
323

324
func initAuthJob(host string) utils.DockerJob {
8✔
325
        return utils.DockerJob{
8✔
326
                Image: utils.Config.Auth.Image,
8✔
327
                Env: []string{
8✔
328
                        "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
8✔
329
                        "GOTRUE_LOG_LEVEL=error",
8✔
330
                        "GOTRUE_DB_DRIVER=postgres",
8✔
331
                        fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:5432/postgres", utils.Config.Db.Password, host),
8✔
332
                        "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
8✔
333
                        "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
8✔
334
                },
8✔
335
                Cmd: []string{"gotrue", "migrate"},
8✔
336
        }
8✔
337
}
8✔
338

339
func initSchema15(ctx context.Context, host string) error {
10✔
340
        // Apply service migrations
10✔
341
        var initJobs []utils.DockerJob
10✔
342
        if utils.Config.Realtime.Enabled {
18✔
343
                jwks, err := utils.Config.Auth.ResolveJWKS(context.Background())
8✔
344
                if err != nil {
8✔
345
                        return err
×
346
                }
×
347
                initJobs = append(initJobs, initRealtimeJob(host, jwks))
8✔
348
        }
349
        if utils.Config.Storage.Enabled {
18✔
350
                initJobs = append(initJobs, initStorageJob(host))
8✔
351
        }
8✔
352
        if utils.Config.Auth.Enabled {
18✔
353
                initJobs = append(initJobs, initAuthJob(host))
8✔
354
        }
8✔
355
        logger := utils.GetDebugLogger()
10✔
356
        for _, job := range initJobs {
30✔
357
                if err := utils.DockerRunJob(ctx, job, io.Discard, logger); err != nil {
22✔
358
                        return err
2✔
359
                }
2✔
360
        }
361
        return nil
8✔
362
}
363

364
func SetupLocalDatabase(ctx context.Context, version string, fsys afero.Fs, w io.Writer, options ...func(*pgx.ConnConfig)) error {
8✔
365
        conn, err := utils.ConnectLocalPostgres(ctx, pgconn.Config{}, options...)
8✔
366
        if err != nil {
9✔
367
                return err
1✔
368
        }
1✔
369
        defer conn.Close(context.Background())
7✔
370
        if err := SetupDatabase(ctx, conn, utils.DbId, w, fsys); err != nil {
9✔
371
                return err
2✔
372
        }
2✔
373
        return apply.MigrateAndSeed(ctx, version, conn, fsys)
5✔
374
}
375

376
func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error {
15✔
377
        if err := initSchema(ctx, conn, host, w); err != nil {
19✔
378
                return err
4✔
379
        }
4✔
380
        // Create vault secrets first so roles.sql can reference them
381
        if err := vault.UpsertVaultSecrets(ctx, utils.Config.Db.Vault, conn); err != nil {
11✔
382
                return err
×
383
        }
×
384
        err := migration.SeedGlobals(ctx, []string{utils.CustomRolesPath}, conn, afero.NewIOFS(fsys))
11✔
385
        if errors.Is(err, os.ErrNotExist) {
19✔
386
                return nil
8✔
387
        }
8✔
388
        return err
3✔
389
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc