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

supabase / cli / 19497490251

19 Nov 2025 10:07AM UTC coverage: 55.082% (+0.006%) from 55.076%
19497490251

Pull #4420

github

web-flow
Merge 7439d428c into 28425f504
Pull Request #4420: feat: support append mode when updating network restrictions

25 of 38 new or added lines in 3 files covered. (65.79%)

57 existing lines in 3 files now uncovered.

6530 of 11855 relevant lines covered (55.08%)

6.26 hits per line

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

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

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

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

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

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

UNCOV
228
func IsUnhealthyError(err error) bool {
×
UNCOV
229
        // Health check always returns a joinError
×
UNCOV
230
        _, ok := err.(interface{ Unwrap() []error })
×
UNCOV
231
        return ok
×
232
}
×
233

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

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

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

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

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

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

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

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

372
func SetupDatabase(ctx context.Context, conn *pgx.Conn, host string, w io.Writer, fsys afero.Fs) error {
15✔
373
        if err := initSchema(ctx, conn, host, w); err != nil {
19✔
374
                return err
4✔
375
        }
4✔
376
        // Create vault secrets first so roles.sql can reference them
377
        if err := vault.UpsertVaultSecrets(ctx, utils.Config.Db.Vault, conn); err != nil {
11✔
UNCOV
378
                return err
×
UNCOV
379
        }
×
380
        err := migration.SeedGlobals(ctx, []string{utils.CustomRolesPath}, conn, afero.NewIOFS(fsys))
11✔
381
        if errors.Is(err, os.ErrNotExist) {
19✔
382
                return nil
8✔
383
        }
8✔
384
        return err
3✔
385
}
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