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

supabase / cli / 18940990217

30 Oct 2025 12:42PM UTC coverage: 54.676% (-0.03%) from 54.703%
18940990217

Pull #4388

github

web-flow
Merge 8164a1f1b into 11bd66df4
Pull Request #4388: feat: add configurable JWT issuer for local auth development

3 of 5 new or added lines in 1 file covered. (60.0%)

5 existing lines in 1 file now uncovered.

6390 of 11687 relevant lines covered (54.68%)

6.13 hits per line

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

65.38
/internal/start/start.go
1
package start
2

3
import (
4
        "bytes"
5
        "context"
6
        _ "embed"
7
        "fmt"
8
        "net"
9
        "net/url"
10
        "os"
11
        "path"
12
        "path/filepath"
13
        "slices"
14
        "strconv"
15
        "strings"
16
        "text/template"
17
        "time"
18

19
        "github.com/docker/docker/api/types/container"
20
        "github.com/docker/docker/api/types/network"
21
        "github.com/docker/docker/client"
22
        "github.com/docker/go-connections/nat"
23
        "github.com/go-errors/errors"
24
        "github.com/jackc/pgconn"
25
        "github.com/jackc/pgx/v4"
26
        "github.com/spf13/afero"
27
        "github.com/supabase/cli/internal/db/start"
28
        "github.com/supabase/cli/internal/functions/serve"
29
        "github.com/supabase/cli/internal/seed/buckets"
30
        "github.com/supabase/cli/internal/services"
31
        "github.com/supabase/cli/internal/status"
32
        "github.com/supabase/cli/internal/utils"
33
        "github.com/supabase/cli/internal/utils/flags"
34
        "github.com/supabase/cli/pkg/config"
35
)
36

37
func Run(ctx context.Context, fsys afero.Fs, excludedContainers []string, ignoreHealthCheck bool) error {
3✔
38
        // Sanity checks.
3✔
39
        {
6✔
40
                if err := flags.LoadConfig(fsys); err != nil {
4✔
41
                        return err
1✔
42
                }
1✔
43
                if err := utils.AssertSupabaseDbIsRunning(); err == nil {
3✔
44
                        fmt.Fprintln(os.Stderr, utils.Aqua("supabase start")+" is already running.")
1✔
45
                        names := status.CustomName{}
1✔
46
                        return status.Run(ctx, names, utils.OutputPretty, fsys)
1✔
47
                } else if !errors.Is(err, utils.ErrNotRunning) {
3✔
48
                        return err
1✔
49
                }
1✔
50
                if err := flags.LoadProjectRef(fsys); err == nil {
×
51
                        _ = services.CheckVersions(ctx, fsys)
×
52
                }
×
53
        }
54

55
        dbConfig := pgconn.Config{
×
56
                Host:     utils.DbId,
×
57
                Port:     5432,
×
58
                User:     "postgres",
×
59
                Password: utils.Config.Db.Password,
×
60
                Database: "postgres",
×
61
        }
×
62
        if err := run(ctx, fsys, excludedContainers, dbConfig); err != nil {
×
63
                if ignoreHealthCheck && start.IsUnhealthyError(err) {
×
64
                        fmt.Fprintln(os.Stderr, err)
×
65
                } else {
×
66
                        if err := utils.DockerRemoveAll(context.Background(), os.Stderr, utils.Config.ProjectId); err != nil {
×
67
                                fmt.Fprintln(os.Stderr, err)
×
68
                        }
×
69
                        return err
×
70
                }
71
        }
72

73
        fmt.Fprintf(os.Stderr, "Started %s local development setup.\n\n", utils.Aqua("supabase"))
×
74
        status.PrettyPrint(os.Stdout, excludedContainers...)
×
75
        return nil
×
76
}
77

78
type kongConfig struct {
79
        GotrueId      string
80
        RestId        string
81
        RealtimeId    string
82
        StorageId     string
83
        StudioId      string
84
        PgmetaId      string
85
        EdgeRuntimeId string
86
        LogflareId    string
87
        PoolerId      string
88
        ApiHost       string
89
        ApiPort       uint16
90
        BearerToken   string
91
        QueryToken    string
92
}
93

94
var (
95
        //go:embed templates/kong.yml
96
        kongConfigEmbed    string
97
        kongConfigTemplate = template.Must(template.New("kongConfig").Parse(kongConfigEmbed))
98

99
        //go:embed templates/custom_nginx.template
100
        nginxConfigEmbed string
101
        // Hardcoded configs which match nginxConfigEmbed
102
        nginxEmailTemplateDir   = "/home/kong/templates/email"
103
        nginxTemplateServerPort = 8088
104
)
105

106
type vectorConfig struct {
107
        ApiKey        string
108
        VectorId      string
109
        LogflareId    string
110
        KongId        string
111
        GotrueId      string
112
        RestId        string
113
        RealtimeId    string
114
        StorageId     string
115
        EdgeRuntimeId string
116
        DbId          string
117
}
118

119
var (
120
        //go:embed templates/vector.yaml
121
        vectorConfigEmbed    string
122
        vectorConfigTemplate = template.Must(template.New("vectorConfig").Parse(vectorConfigEmbed))
123
)
124

125
type poolerTenant struct {
126
        DbHost            string
127
        DbPort            uint16
128
        DbDatabase        string
129
        DbPassword        string
130
        ExternalId        string
131
        ModeType          config.PoolMode
132
        DefaultMaxClients uint
133
        DefaultPoolSize   uint
134
}
135

136
var (
137
        //go:embed templates/pooler.exs
138
        poolerTenantEmbed    string
139
        poolerTenantTemplate = template.Must(template.New("poolerTenant").Parse(poolerTenantEmbed))
140
)
141

142
var serviceTimeout = 30 * time.Second
143

144
func run(ctx context.Context, fsys afero.Fs, excludedContainers []string, dbConfig pgconn.Config, options ...func(*pgx.ConnConfig)) error {
2✔
145
        excluded := make(map[string]bool)
2✔
146
        for _, name := range excludedContainers {
17✔
147
                excluded[name] = true
15✔
148
        }
15✔
149

150
        jwks, err := utils.Config.Auth.ResolveJWKS(ctx)
2✔
151
        if err != nil {
2✔
152
                return err
×
153
        }
×
154

155
        // Start Postgres.
156
        if dbConfig.Host == utils.DbId {
4✔
157
                if err := start.StartDatabase(ctx, "", fsys, os.Stderr, options...); err != nil {
2✔
158
                        return err
×
159
                }
×
160
        }
161

162
        var started []string
2✔
163
        var isStorageEnabled = utils.Config.Storage.Enabled && !isContainerExcluded(utils.Config.Storage.Image, excluded)
2✔
164
        var isImgProxyEnabled = utils.Config.Storage.ImageTransformation != nil &&
2✔
165
                utils.Config.Storage.ImageTransformation.Enabled && !isContainerExcluded(utils.Config.Storage.ImgProxyImage, excluded)
2✔
166
        fmt.Fprintln(os.Stderr, "Starting containers...")
2✔
167

2✔
168
        // Start Logflare
2✔
169
        if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.Image, excluded) {
3✔
170
                env := []string{
1✔
171
                        "DB_DATABASE=_supabase",
1✔
172
                        "DB_HOSTNAME=" + dbConfig.Host,
1✔
173
                        fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
1✔
174
                        "DB_SCHEMA=_analytics",
1✔
175
                        "DB_USERNAME=" + utils.SUPERUSER_ROLE,
1✔
176
                        "DB_PASSWORD=" + dbConfig.Password,
1✔
177
                        "LOGFLARE_MIN_CLUSTER_SIZE=1",
1✔
178
                        "LOGFLARE_SINGLE_TENANT=true",
1✔
179
                        "LOGFLARE_SUPABASE_MODE=true",
1✔
180
                        "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey,
1✔
181
                        "LOGFLARE_LOG_LEVEL=warn",
1✔
182
                        "LOGFLARE_NODE_HOST=127.0.0.1",
1✔
183
                        "LOGFLARE_FEATURE_FLAG_OVERRIDE='multibackend=true'",
1✔
184
                        "RELEASE_COOKIE=cookie",
1✔
185
                }
1✔
186
                bind := []string{}
1✔
187

1✔
188
                switch utils.Config.Analytics.Backend {
1✔
189
                case config.LogflareBigQuery:
×
190
                        workdir, err := os.Getwd()
×
191
                        if err != nil {
×
192
                                return errors.Errorf("failed to get working directory: %w", err)
×
193
                        }
×
194
                        hostJwtPath := filepath.Join(workdir, utils.Config.Analytics.GcpJwtPath)
×
195
                        bind = append(bind, hostJwtPath+":/opt/app/rel/logflare/bin/gcloud.json")
×
196
                        // This is hardcoded in studio frontend
×
197
                        env = append(env,
×
198
                                "GOOGLE_DATASET_ID_APPEND=_prod",
×
199
                                "GOOGLE_PROJECT_ID="+utils.Config.Analytics.GcpProjectId,
×
200
                                "GOOGLE_PROJECT_NUMBER="+utils.Config.Analytics.GcpProjectNumber,
×
201
                        )
×
202
                case config.LogflarePostgres:
1✔
203
                        env = append(env,
1✔
204
                                fmt.Sprintf("POSTGRES_BACKEND_URL=postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, "_supabase"),
1✔
205
                                "POSTGRES_BACKEND_SCHEMA=_analytics",
1✔
206
                        )
1✔
207
                }
208

209
                if _, err := utils.DockerStart(
1✔
210
                        ctx,
1✔
211
                        container.Config{
1✔
212
                                Hostname: "127.0.0.1",
1✔
213
                                Image:    utils.Config.Analytics.Image,
1✔
214
                                Env:      env,
1✔
215
                                // Original entrypoint conflicts with healthcheck due to 15 seconds sleep:
1✔
216
                                // https://github.com/Logflare/logflare/blob/staging/run.sh#L35
1✔
217
                                Entrypoint: []string{"sh", "-c", `cat <<'EOF' > run.sh && sh run.sh
1✔
218
./logflare eval Logflare.Release.migrate
1✔
219
./logflare start --sname logflare
1✔
220
EOF
1✔
221
`},
1✔
222
                                Healthcheck: &container.HealthConfig{
1✔
223
                                        Test: []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null",
1✔
224
                                                "http://127.0.0.1:4000/health",
1✔
225
                                        },
1✔
226
                                        Interval:    10 * time.Second,
1✔
227
                                        Timeout:     2 * time.Second,
1✔
228
                                        Retries:     3,
1✔
229
                                        StartPeriod: 10 * time.Second,
1✔
230
                                },
1✔
231
                                ExposedPorts: nat.PortSet{"4000/tcp": {}},
1✔
232
                        },
1✔
233
                        container.HostConfig{
1✔
234
                                Binds:         bind,
1✔
235
                                PortBindings:  nat.PortMap{"4000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Analytics.Port), 10)}}},
1✔
236
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
237
                        },
1✔
238
                        network.NetworkingConfig{
1✔
239
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
240
                                        utils.NetId: {
1✔
241
                                                Aliases: utils.LogflareAliases,
1✔
242
                                        },
1✔
243
                                },
1✔
244
                        },
1✔
245
                        utils.LogflareId,
1✔
246
                ); err != nil {
1✔
247
                        return err
×
248
                }
×
249
                started = append(started, utils.LogflareId)
1✔
250
        }
251

252
        // Start vector
253
        if utils.Config.Analytics.Enabled && !isContainerExcluded(utils.Config.Analytics.VectorImage, excluded) {
3✔
254
                var vectorConfigBuf bytes.Buffer
1✔
255
                if err := vectorConfigTemplate.Option("missingkey=error").Execute(&vectorConfigBuf, vectorConfig{
1✔
256
                        ApiKey:        utils.Config.Analytics.ApiKey,
1✔
257
                        VectorId:      utils.VectorId,
1✔
258
                        LogflareId:    utils.LogflareId,
1✔
259
                        KongId:        utils.KongId,
1✔
260
                        GotrueId:      utils.GotrueId,
1✔
261
                        RestId:        utils.RestId,
1✔
262
                        RealtimeId:    utils.RealtimeId,
1✔
263
                        StorageId:     utils.StorageId,
1✔
264
                        EdgeRuntimeId: utils.EdgeRuntimeId,
1✔
265
                        DbId:          utils.DbId,
1✔
266
                }); err != nil {
1✔
267
                        return errors.Errorf("failed to exec template: %w", err)
×
268
                }
×
269
                var binds, env, securityOpts []string
1✔
270
                // Special case for GitLab pipeline
1✔
271
                parsed, err := client.ParseHostURL(utils.Docker.DaemonHost())
1✔
272
                if err != nil {
1✔
273
                        return errors.Errorf("failed to parse docker host: %w", err)
×
274
                }
×
275
                // Ref: https://vector.dev/docs/reference/configuration/sources/docker_logs/#docker_host
276
                dindHost := &url.URL{Scheme: "http", Host: net.JoinHostPort(utils.DinDHost, "2375")}
1✔
277
                switch parsed.Scheme {
1✔
278
                case "tcp":
×
279
                        if _, port, err := net.SplitHostPort(parsed.Host); err == nil {
×
280
                                dindHost.Host = net.JoinHostPort(utils.DinDHost, port)
×
281
                        }
×
282
                        env = append(env, "DOCKER_HOST="+dindHost.String())
×
283
                case "npipe":
×
284
                        const dockerDaemonNeededErr = "Analytics on Windows requires Docker daemon exposed on tcp://localhost:2375.\nSee https://supabase.com/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=windows#running-supabase-locally for more details."
×
285
                        fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), dockerDaemonNeededErr)
×
286
                        env = append(env, "DOCKER_HOST="+dindHost.String())
×
287
                case "unix":
×
288
                        if dindHost, err = client.ParseHostURL(client.DefaultDockerHost); err != nil {
×
289
                                return errors.Errorf("failed to parse default host: %w", err)
×
290
                        } else if strings.HasSuffix(parsed.Host, "/.docker/run/docker.sock") {
×
291
                                fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "analytics requires mounting default docker socket:", dindHost.Host)
×
292
                                binds = append(binds, fmt.Sprintf("%[1]s:%[1]s:ro", dindHost.Host))
×
293
                        } else {
×
294
                                // Podman and OrbStack can mount root-less socket without issue
×
295
                                binds = append(binds, fmt.Sprintf("%s:%s:ro", parsed.Host, dindHost.Host))
×
296
                                securityOpts = append(securityOpts, "label:disable")
×
297
                        }
×
298
                }
299
                if _, err := utils.DockerStart(
1✔
300
                        ctx,
1✔
301
                        container.Config{
1✔
302
                                Image: utils.Config.Analytics.VectorImage,
1✔
303
                                Env:   env,
1✔
304
                                Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /etc/vector/vector.yaml && vector --config /etc/vector/vector.yaml
1✔
305
` + vectorConfigBuf.String() + `
1✔
306
EOF
1✔
307
`},
1✔
308
                                Healthcheck: &container.HealthConfig{
1✔
309
                                        Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
1✔
310
                                                "http://127.0.0.1:9001/health",
1✔
311
                                        },
1✔
312
                                        Interval: 10 * time.Second,
1✔
313
                                        Timeout:  2 * time.Second,
1✔
314
                                        Retries:  3,
1✔
315
                                },
1✔
316
                        },
1✔
317
                        container.HostConfig{
1✔
318
                                Binds:         binds,
1✔
319
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
320
                                SecurityOpt:   securityOpts,
1✔
321
                        },
1✔
322
                        network.NetworkingConfig{
1✔
323
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
324
                                        utils.NetId: {
1✔
325
                                                Aliases: utils.VectorAliases,
1✔
326
                                        },
1✔
327
                                },
1✔
328
                        },
1✔
329
                        utils.VectorId,
1✔
330
                ); err != nil {
1✔
331
                        return err
×
332
                }
×
333
                if parsed.Scheme != "npipe" {
2✔
334
                        started = append(started, utils.VectorId)
1✔
335
                }
1✔
336
        }
337

338
        // Start Kong.
339
        if !isContainerExcluded(utils.Config.Api.KongImage, excluded) {
3✔
340
                var kongConfigBuf bytes.Buffer
1✔
341
                if err := kongConfigTemplate.Option("missingkey=error").Execute(&kongConfigBuf, kongConfig{
1✔
342
                        GotrueId:      utils.GotrueId,
1✔
343
                        RestId:        utils.RestId,
1✔
344
                        RealtimeId:    utils.Config.Realtime.TenantId,
1✔
345
                        StorageId:     utils.StorageId,
1✔
346
                        StudioId:      utils.StudioId,
1✔
347
                        PgmetaId:      utils.PgmetaId,
1✔
348
                        EdgeRuntimeId: utils.EdgeRuntimeId,
1✔
349
                        LogflareId:    utils.LogflareId,
1✔
350
                        PoolerId:      utils.PoolerId,
1✔
351
                        ApiHost:       utils.Config.Hostname,
1✔
352
                        ApiPort:       utils.Config.Api.Port,
1✔
353
                        BearerToken: fmt.Sprintf(
1✔
354
                                // If Authorization header is set to a self-minted JWT, we want to pass it down.
1✔
355
                                // Legacy supabase-js may set Authorization header to Bearer <apikey>. We must remove it
1✔
356
                                // to avoid failing JWT validation.
1✔
357
                                // If Authorization header is missing, we want to match against apikey header to set the
1✔
358
                                // default JWT for downstream services.
1✔
359
                                // Finally, the apikey header may be set to a legacy JWT. In that case, we want to copy
1✔
360
                                // it to Authorization header for backwards compatibility.
1✔
361
                                `$((headers.authorization ~= nil and headers.authorization:sub(1, 10) ~= 'Bearer sb_' and headers.authorization) or (headers.apikey == '%s' and 'Bearer %s') or (headers.apikey == '%s' and 'Bearer %s') or headers.apikey)`,
1✔
362
                                utils.Config.Auth.SecretKey.Value,
1✔
363
                                utils.Config.Auth.ServiceRoleKey.Value,
1✔
364
                                utils.Config.Auth.PublishableKey.Value,
1✔
365
                                utils.Config.Auth.AnonKey.Value,
1✔
366
                        ),
1✔
367
                        QueryToken: fmt.Sprintf(
1✔
368
                                `$((query_params.apikey == '%s' and '%s') or (query_params.apikey == '%s' and '%s') or query_params.apikey)`,
1✔
369
                                utils.Config.Auth.SecretKey.Value,
1✔
370
                                utils.Config.Auth.ServiceRoleKey.Value,
1✔
371
                                utils.Config.Auth.PublishableKey.Value,
1✔
372
                                utils.Config.Auth.AnonKey.Value,
1✔
373
                        ),
1✔
374
                }); err != nil {
1✔
375
                        return errors.Errorf("failed to exec template: %w", err)
×
376
                }
×
377

378
                binds := []string{}
1✔
379
                for id, tmpl := range utils.Config.Auth.Email.Template {
1✔
380
                        if len(tmpl.ContentPath) == 0 {
×
381
                                continue
×
382
                        }
383
                        hostPath := tmpl.ContentPath
×
384
                        if !filepath.IsAbs(tmpl.ContentPath) {
×
385
                                var err error
×
386
                                hostPath, err = filepath.Abs(hostPath)
×
387
                                if err != nil {
×
388
                                        return errors.Errorf("failed to resolve absolute path: %w", err)
×
389
                                }
×
390
                        }
391
                        dockerPath := path.Join(nginxEmailTemplateDir, id+filepath.Ext(hostPath))
×
392
                        binds = append(binds, fmt.Sprintf("%s:%s:rw", hostPath, dockerPath))
×
393
                }
394

395
                dockerPort := uint16(8000)
1✔
396
                if utils.Config.Api.Tls.Enabled {
1✔
397
                        dockerPort = 8443
×
398
                }
×
399
                if _, err := utils.DockerStart(
1✔
400
                        ctx,
1✔
401
                        container.Config{
1✔
402
                                Image: utils.Config.Api.KongImage,
1✔
403
                                Env: []string{
1✔
404
                                        "KONG_DATABASE=off",
1✔
405
                                        "KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml",
1✔
406
                                        "KONG_DNS_ORDER=LAST,A,CNAME", // https://github.com/supabase/cli/issues/14
1✔
407
                                        "KONG_PLUGINS=request-transformer,cors",
1✔
408
                                        fmt.Sprintf("KONG_PORT_MAPS=%d:8000", utils.Config.Api.Port),
1✔
409
                                        // Need to increase the nginx buffers in kong to avoid it rejecting the rather
1✔
410
                                        // sizeable response headers azure can generate
1✔
411
                                        // Ref: https://github.com/Kong/kong/issues/3974#issuecomment-482105126
1✔
412
                                        "KONG_NGINX_PROXY_PROXY_BUFFER_SIZE=160k",
1✔
413
                                        "KONG_NGINX_PROXY_PROXY_BUFFERS=64 160k",
1✔
414
                                        "KONG_NGINX_WORKER_PROCESSES=1",
1✔
415
                                        // Use modern TLS certificate
1✔
416
                                        "KONG_SSL_CERT=/home/kong/localhost.crt",
1✔
417
                                        "KONG_SSL_CERT_KEY=/home/kong/localhost.key",
1✔
418
                                },
1✔
419
                                Entrypoint: []string{"sh", "-c", `cat <<'EOF' > /home/kong/kong.yml && \
1✔
420
cat <<'EOF' > /home/kong/custom_nginx.template && \
1✔
421
cat <<'EOF' > /home/kong/localhost.crt && \
1✔
422
cat <<'EOF' > /home/kong/localhost.key && \
1✔
423
./docker-entrypoint.sh kong docker-start --nginx-conf /home/kong/custom_nginx.template
1✔
424
` + kongConfigBuf.String() + `
1✔
425
EOF
1✔
426
` + nginxConfigEmbed + `
1✔
427
EOF
1✔
428
` + string(utils.Config.Api.Tls.CertContent) + `
1✔
429
EOF
1✔
430
` + string(utils.Config.Api.Tls.KeyContent) + `
1✔
431
EOF
1✔
432
`},
1✔
433
                                ExposedPorts: nat.PortSet{
1✔
434
                                        "8000/tcp": {},
1✔
435
                                        "8443/tcp": {},
1✔
436
                                        nat.Port(fmt.Sprintf("%d/tcp", nginxTemplateServerPort)): {},
1✔
437
                                },
1✔
438
                        },
1✔
439
                        container.HostConfig{
1✔
440
                                Binds: binds,
1✔
441
                                PortBindings: nat.PortMap{nat.Port(fmt.Sprintf("%d/tcp", dockerPort)): []nat.PortBinding{{
1✔
442
                                        HostPort: strconv.FormatUint(uint64(utils.Config.Api.Port), 10)},
1✔
443
                                }},
1✔
444
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
445
                        },
1✔
446
                        network.NetworkingConfig{
1✔
447
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
448
                                        utils.NetId: {
1✔
449
                                                Aliases: utils.KongAliases,
1✔
450
                                        },
1✔
451
                                },
1✔
452
                        },
1✔
453
                        utils.KongId,
1✔
454
                ); err != nil {
1✔
455
                        return err
×
456
                }
×
457
                started = append(started, utils.KongId)
1✔
458
        }
459

460
        // Start GoTrue.
461
        if utils.Config.Auth.Enabled && !isContainerExcluded(utils.Config.Auth.Image, excluded) {
3✔
462
                var testOTP bytes.Buffer
1✔
463
                if len(utils.Config.Auth.Sms.TestOTP) > 0 {
1✔
464
                        formatMapForEnvConfig(utils.Config.Auth.Sms.TestOTP, &testOTP)
×
465
                }
×
466

467
                jwtIssuer := utils.GetApiUrl("/auth/v1")
1✔
468
                if utils.Config.Auth.JwtIssuer != "" {
1✔
NEW
469
                        jwtIssuer = utils.Config.Auth.JwtIssuer
×
NEW
470
                }
×
471

472
                env := []string{
1✔
473
                        "API_EXTERNAL_URL=" + utils.Config.Api.ExternalUrl,
1✔
474

1✔
475
                        "GOTRUE_API_HOST=0.0.0.0",
1✔
476
                        "GOTRUE_API_PORT=9999",
1✔
477

1✔
478
                        "GOTRUE_DB_DRIVER=postgres",
1✔
479
                        fmt.Sprintf("GOTRUE_DB_DATABASE_URL=postgresql://supabase_auth_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
1✔
480

1✔
481
                        "GOTRUE_SITE_URL=" + utils.Config.Auth.SiteUrl,
1✔
482
                        "GOTRUE_URI_ALLOW_LIST=" + strings.Join(utils.Config.Auth.AdditionalRedirectUrls, ","),
1✔
483
                        fmt.Sprintf("GOTRUE_DISABLE_SIGNUP=%v", !utils.Config.Auth.EnableSignup),
1✔
484

1✔
485
                        "GOTRUE_JWT_ADMIN_ROLES=service_role",
1✔
486
                        "GOTRUE_JWT_AUD=authenticated",
1✔
487
                        "GOTRUE_JWT_DEFAULT_GROUP_NAME=authenticated",
1✔
488
                        fmt.Sprintf("GOTRUE_JWT_EXP=%v", utils.Config.Auth.JwtExpiry),
1✔
489
                        "GOTRUE_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
490
                        "GOTRUE_JWT_ISSUER=" + jwtIssuer,
1✔
491

1✔
492
                        fmt.Sprintf("GOTRUE_EXTERNAL_EMAIL_ENABLED=%v", utils.Config.Auth.Email.EnableSignup),
1✔
493
                        fmt.Sprintf("GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=%v", utils.Config.Auth.Email.DoubleConfirmChanges),
1✔
494
                        fmt.Sprintf("GOTRUE_MAILER_AUTOCONFIRM=%v", !utils.Config.Auth.Email.EnableConfirmations),
1✔
495
                        fmt.Sprintf("GOTRUE_MAILER_OTP_LENGTH=%v", utils.Config.Auth.Email.OtpLength),
1✔
496
                        fmt.Sprintf("GOTRUE_MAILER_OTP_EXP=%v", utils.Config.Auth.Email.OtpExpiry),
1✔
497

1✔
498
                        fmt.Sprintf("GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=%v", utils.Config.Auth.EnableAnonymousSignIns),
1✔
499

1✔
500
                        fmt.Sprintf("GOTRUE_SMTP_MAX_FREQUENCY=%v", utils.Config.Auth.Email.MaxFrequency),
1✔
501

1✔
502
                        "GOTRUE_MAILER_URLPATHS_INVITE=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
503
                        "GOTRUE_MAILER_URLPATHS_CONFIRMATION=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
504
                        "GOTRUE_MAILER_URLPATHS_RECOVERY=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
505
                        "GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE=" + utils.GetApiUrl("/auth/v1/verify"),
1✔
506
                        "GOTRUE_RATE_LIMIT_EMAIL_SENT=360000",
1✔
507

1✔
508
                        fmt.Sprintf("GOTRUE_EXTERNAL_PHONE_ENABLED=%v", utils.Config.Auth.Sms.EnableSignup),
1✔
509
                        fmt.Sprintf("GOTRUE_SMS_AUTOCONFIRM=%v", !utils.Config.Auth.Sms.EnableConfirmations),
1✔
510
                        fmt.Sprintf("GOTRUE_SMS_MAX_FREQUENCY=%v", utils.Config.Auth.Sms.MaxFrequency),
1✔
511
                        "GOTRUE_SMS_OTP_EXP=6000",
1✔
512
                        "GOTRUE_SMS_OTP_LENGTH=6",
1✔
513
                        fmt.Sprintf("GOTRUE_SMS_TEMPLATE=%v", utils.Config.Auth.Sms.Template),
1✔
514
                        "GOTRUE_SMS_TEST_OTP=" + testOTP.String(),
1✔
515

1✔
516
                        fmt.Sprintf("GOTRUE_PASSWORD_MIN_LENGTH=%v", utils.Config.Auth.MinimumPasswordLength),
1✔
517
                        fmt.Sprintf("GOTRUE_PASSWORD_REQUIRED_CHARACTERS=%v", utils.Config.Auth.PasswordRequirements.ToChar()),
1✔
518
                        fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=%v", utils.Config.Auth.EnableRefreshTokenRotation),
1✔
519
                        fmt.Sprintf("GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=%v", utils.Config.Auth.RefreshTokenReuseInterval),
1✔
520
                        fmt.Sprintf("GOTRUE_SECURITY_MANUAL_LINKING_ENABLED=%v", utils.Config.Auth.EnableManualLinking),
1✔
521
                        fmt.Sprintf("GOTRUE_SECURITY_UPDATE_PASSWORD_REQUIRE_REAUTHENTICATION=%v", utils.Config.Auth.Email.SecurePasswordChange),
1✔
522
                        fmt.Sprintf("GOTRUE_MFA_PHONE_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.Phone.EnrollEnabled),
1✔
523
                        fmt.Sprintf("GOTRUE_MFA_PHONE_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.Phone.VerifyEnabled),
1✔
524
                        fmt.Sprintf("GOTRUE_MFA_TOTP_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.TOTP.EnrollEnabled),
1✔
525
                        fmt.Sprintf("GOTRUE_MFA_TOTP_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.TOTP.VerifyEnabled),
1✔
526
                        fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_ENROLL_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.EnrollEnabled),
1✔
527
                        fmt.Sprintf("GOTRUE_MFA_WEB_AUTHN_VERIFY_ENABLED=%v", utils.Config.Auth.MFA.WebAuthn.VerifyEnabled),
1✔
528
                        fmt.Sprintf("GOTRUE_MFA_MAX_ENROLLED_FACTORS=%v", utils.Config.Auth.MFA.MaxEnrolledFactors),
1✔
529

1✔
530
                        // Add rate limit configurations
1✔
531
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_ANONYMOUS_USERS=%v", utils.Config.Auth.RateLimit.AnonymousUsers),
1✔
532
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_TOKEN_REFRESH=%v", utils.Config.Auth.RateLimit.TokenRefresh),
1✔
533
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_OTP=%v", utils.Config.Auth.RateLimit.SignInSignUps),
1✔
534
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_VERIFY=%v", utils.Config.Auth.RateLimit.TokenVerifications),
1✔
535
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_SMS_SENT=%v", utils.Config.Auth.RateLimit.SmsSent),
1✔
536
                        fmt.Sprintf("GOTRUE_RATE_LIMIT_WEB3=%v", utils.Config.Auth.RateLimit.Web3),
1✔
537
                }
1✔
538

1✔
539
                // Since signing key is validated by ResolveJWKS, simply read the key file.
1✔
540
                if keys, err := afero.ReadFile(fsys, utils.Config.Auth.SigningKeysPath); err == nil && len(keys) > 0 {
1✔
541
                        env = append(env, "GOTRUE_JWT_KEYS="+string(keys))
×
542
                }
×
543

544
                if utils.Config.Auth.Email.Smtp != nil && utils.Config.Auth.Email.Smtp.Enabled {
1✔
545
                        env = append(env,
×
546
                                fmt.Sprintf("GOTRUE_RATE_LIMIT_EMAIL_SENT=%v", utils.Config.Auth.RateLimit.EmailSent),
×
547
                                fmt.Sprintf("GOTRUE_SMTP_HOST=%s", utils.Config.Auth.Email.Smtp.Host),
×
548
                                fmt.Sprintf("GOTRUE_SMTP_PORT=%d", utils.Config.Auth.Email.Smtp.Port),
×
549
                                fmt.Sprintf("GOTRUE_SMTP_USER=%s", utils.Config.Auth.Email.Smtp.User),
×
550
                                fmt.Sprintf("GOTRUE_SMTP_PASS=%s", utils.Config.Auth.Email.Smtp.Pass.Value),
×
551
                                fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Auth.Email.Smtp.AdminEmail),
×
552
                                fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Auth.Email.Smtp.SenderName),
×
553
                        )
×
554
                } else if utils.Config.Inbucket.Enabled {
2✔
555
                        env = append(env,
1✔
556
                                "GOTRUE_SMTP_HOST="+utils.InbucketId,
1✔
557
                                "GOTRUE_SMTP_PORT=1025",
1✔
558
                                fmt.Sprintf("GOTRUE_SMTP_ADMIN_EMAIL=%s", utils.Config.Inbucket.AdminEmail),
1✔
559
                                fmt.Sprintf("GOTRUE_SMTP_SENDER_NAME=%s", utils.Config.Inbucket.SenderName),
1✔
560
                        )
1✔
561
                }
1✔
562

563
                if utils.Config.Auth.Sessions.Timebox > 0 {
1✔
564
                        env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_TIMEBOX=%v", utils.Config.Auth.Sessions.Timebox))
×
565
                }
×
566
                if utils.Config.Auth.Sessions.InactivityTimeout > 0 {
1✔
567
                        env = append(env, fmt.Sprintf("GOTRUE_SESSIONS_INACTIVITY_TIMEOUT=%v", utils.Config.Auth.Sessions.InactivityTimeout))
×
568
                }
×
569

570
                for id, tmpl := range utils.Config.Auth.Email.Template {
1✔
571
                        if len(tmpl.ContentPath) > 0 {
×
572
                                env = append(env, fmt.Sprintf("GOTRUE_MAILER_TEMPLATES_%s=http://%s:%d/email/%s",
×
573
                                        strings.ToUpper(id),
×
574
                                        utils.KongId,
×
575
                                        nginxTemplateServerPort,
×
576
                                        id+filepath.Ext(tmpl.ContentPath),
×
577
                                ))
×
578
                        }
×
579
                        if tmpl.Subject != nil {
×
580
                                env = append(env, fmt.Sprintf("GOTRUE_MAILER_SUBJECTS_%s=%s",
×
581
                                        strings.ToUpper(id),
×
582
                                        *tmpl.Subject,
×
583
                                ))
×
584
                        }
×
585
                }
586

587
                switch {
1✔
588
                case utils.Config.Auth.Sms.Twilio.Enabled:
×
589
                        env = append(
×
590
                                env,
×
591
                                "GOTRUE_SMS_PROVIDER=twilio",
×
592
                                "GOTRUE_SMS_TWILIO_ACCOUNT_SID="+utils.Config.Auth.Sms.Twilio.AccountSid,
×
593
                                "GOTRUE_SMS_TWILIO_AUTH_TOKEN="+utils.Config.Auth.Sms.Twilio.AuthToken.Value,
×
594
                                "GOTRUE_SMS_TWILIO_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.Twilio.MessageServiceSid,
×
595
                        )
×
596
                case utils.Config.Auth.Sms.TwilioVerify.Enabled:
×
597
                        env = append(
×
598
                                env,
×
599
                                "GOTRUE_SMS_PROVIDER=twilio_verify",
×
600
                                "GOTRUE_SMS_TWILIO_VERIFY_ACCOUNT_SID="+utils.Config.Auth.Sms.TwilioVerify.AccountSid,
×
601
                                "GOTRUE_SMS_TWILIO_VERIFY_AUTH_TOKEN="+utils.Config.Auth.Sms.TwilioVerify.AuthToken.Value,
×
602
                                "GOTRUE_SMS_TWILIO_VERIFY_MESSAGE_SERVICE_SID="+utils.Config.Auth.Sms.TwilioVerify.MessageServiceSid,
×
603
                        )
×
604
                case utils.Config.Auth.Sms.Messagebird.Enabled:
×
605
                        env = append(
×
606
                                env,
×
607
                                "GOTRUE_SMS_PROVIDER=messagebird",
×
608
                                "GOTRUE_SMS_MESSAGEBIRD_ACCESS_KEY="+utils.Config.Auth.Sms.Messagebird.AccessKey.Value,
×
609
                                "GOTRUE_SMS_MESSAGEBIRD_ORIGINATOR="+utils.Config.Auth.Sms.Messagebird.Originator,
×
610
                        )
×
611
                case utils.Config.Auth.Sms.Textlocal.Enabled:
×
612
                        env = append(
×
613
                                env,
×
614
                                "GOTRUE_SMS_PROVIDER=textlocal",
×
615
                                "GOTRUE_SMS_TEXTLOCAL_API_KEY="+utils.Config.Auth.Sms.Textlocal.ApiKey.Value,
×
616
                                "GOTRUE_SMS_TEXTLOCAL_SENDER="+utils.Config.Auth.Sms.Textlocal.Sender,
×
617
                        )
×
618
                case utils.Config.Auth.Sms.Vonage.Enabled:
×
619
                        env = append(
×
620
                                env,
×
621
                                "GOTRUE_SMS_PROVIDER=vonage",
×
622
                                "GOTRUE_SMS_VONAGE_API_KEY="+utils.Config.Auth.Sms.Vonage.ApiKey,
×
623
                                "GOTRUE_SMS_VONAGE_API_SECRET="+utils.Config.Auth.Sms.Vonage.ApiSecret.Value,
×
624
                                "GOTRUE_SMS_VONAGE_FROM="+utils.Config.Auth.Sms.Vonage.From,
×
625
                        )
×
626
                }
627

628
                if captcha := utils.Config.Auth.Captcha; captcha != nil {
1✔
629
                        env = append(
×
630
                                env,
×
631
                                fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_ENABLED=%v", captcha.Enabled),
×
632
                                fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_PROVIDER=%v", captcha.Provider),
×
633
                                fmt.Sprintf("GOTRUE_SECURITY_CAPTCHA_SECRET=%v", captcha.Secret.Value),
×
634
                        )
×
635
                }
×
636

637
                if hook := utils.Config.Auth.Hook.MFAVerificationAttempt; hook != nil && hook.Enabled {
1✔
638
                        env = append(
×
639
                                env,
×
640
                                "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_ENABLED=true",
×
641
                                "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_URI="+hook.URI,
×
642
                                "GOTRUE_HOOK_MFA_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value,
×
643
                        )
×
644
                }
×
645
                if hook := utils.Config.Auth.Hook.PasswordVerificationAttempt; hook != nil && hook.Enabled {
1✔
646
                        env = append(
×
647
                                env,
×
648
                                "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED=true",
×
649
                                "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="+hook.URI,
×
650
                                "GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_SECRETS="+hook.Secrets.Value,
×
651
                        )
×
652
                }
×
653
                if hook := utils.Config.Auth.Hook.CustomAccessToken; hook != nil && hook.Enabled {
1✔
654
                        env = append(
×
655
                                env,
×
656
                                "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_ENABLED=true",
×
657
                                "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_URI="+hook.URI,
×
658
                                "GOTRUE_HOOK_CUSTOM_ACCESS_TOKEN_SECRETS="+hook.Secrets.Value,
×
659
                        )
×
660
                }
×
661
                if hook := utils.Config.Auth.Hook.SendSMS; hook != nil && hook.Enabled {
1✔
662
                        env = append(
×
663
                                env,
×
664
                                "GOTRUE_HOOK_SEND_SMS_ENABLED=true",
×
665
                                "GOTRUE_HOOK_SEND_SMS_URI="+hook.URI,
×
666
                                "GOTRUE_HOOK_SEND_SMS_SECRETS="+hook.Secrets.Value,
×
667
                        )
×
668
                }
×
669
                if hook := utils.Config.Auth.Hook.SendEmail; hook != nil && hook.Enabled {
1✔
670
                        env = append(
×
671
                                env,
×
672
                                "GOTRUE_HOOK_SEND_EMAIL_ENABLED=true",
×
673
                                "GOTRUE_HOOK_SEND_EMAIL_URI="+hook.URI,
×
674
                                "GOTRUE_HOOK_SEND_EMAIL_SECRETS="+hook.Secrets.Value,
×
675
                        )
×
676
                }
×
677
                if hook := utils.Config.Auth.Hook.BeforeUserCreated; hook != nil && hook.Enabled {
1✔
678
                        env = append(
×
679
                                env,
×
680
                                "GOTRUE_HOOK_BEFORE_USER_CREATED_ENABLED=true",
×
681
                                "GOTRUE_HOOK_BEFORE_USER_CREATED_URI="+hook.URI,
×
682
                                "GOTRUE_HOOK_BEFORE_USER_CREATED_SECRETS="+hook.Secrets.Value,
×
683
                        )
×
684
                }
×
685

686
                if utils.Config.Auth.MFA.Phone.EnrollEnabled || utils.Config.Auth.MFA.Phone.VerifyEnabled {
1✔
687
                        env = append(
×
688
                                env,
×
689
                                "GOTRUE_MFA_PHONE_TEMPLATE="+utils.Config.Auth.MFA.Phone.Template,
×
690
                                fmt.Sprintf("GOTRUE_MFA_PHONE_OTP_LENGTH=%v", utils.Config.Auth.MFA.Phone.OtpLength),
×
691
                                fmt.Sprintf("GOTRUE_MFA_PHONE_MAX_FREQUENCY=%v", utils.Config.Auth.MFA.Phone.MaxFrequency),
×
692
                        )
×
693
                }
×
694

695
                for name, config := range utils.Config.Auth.External {
2✔
696
                        env = append(
1✔
697
                                env,
1✔
698
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_ENABLED=%v", strings.ToUpper(name), config.Enabled),
1✔
699
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_CLIENT_ID=%s", strings.ToUpper(name), config.ClientId),
1✔
700
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_SECRET=%s", strings.ToUpper(name), config.Secret.Value),
1✔
701
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_SKIP_NONCE_CHECK=%t", strings.ToUpper(name), config.SkipNonceCheck),
1✔
702
                                fmt.Sprintf("GOTRUE_EXTERNAL_%s_EMAIL_OPTIONAL=%t", strings.ToUpper(name), config.EmailOptional),
1✔
703
                        )
1✔
704

1✔
705
                        redirectUri := config.RedirectUri
1✔
706
                        if redirectUri == "" {
2✔
707
                                redirectUri = utils.GetApiUrl("/auth/v1/callback")
1✔
708
                        }
1✔
709
                        env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_REDIRECT_URI=%s", strings.ToUpper(name), redirectUri))
1✔
710

1✔
711
                        if config.Url != "" {
1✔
712
                                env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_%s_URL=%s", strings.ToUpper(name), config.Url))
×
713
                        }
×
714
                }
715
                env = append(env, fmt.Sprintf("GOTRUE_EXTERNAL_WEB3_SOLANA_ENABLED=%v", utils.Config.Auth.Web3.Solana.Enabled))
1✔
716

1✔
717
                // OAuth server configuration
1✔
718
                if utils.Config.Auth.OAuthServer.Enabled {
1✔
719
                        env = append(env,
×
720
                                fmt.Sprintf("GOTRUE_OAUTH_SERVER_ENABLED=%v", utils.Config.Auth.OAuthServer.Enabled),
×
721
                                "GOTRUE_OAUTH_SERVER_AUTHORIZATION_PATH="+utils.Config.Auth.OAuthServer.AuthorizationUrlPath,
×
722
                                fmt.Sprintf("GOTRUE_OAUTH_SERVER_ALLOW_DYNAMIC_REGISTRATION=%v", utils.Config.Auth.OAuthServer.AllowDynamicRegistration),
×
723
                        )
×
724
                }
×
725

726
                if _, err := utils.DockerStart(
1✔
727
                        ctx,
1✔
728
                        container.Config{
1✔
729
                                Image:        utils.Config.Auth.Image,
1✔
730
                                Env:          env,
1✔
731
                                ExposedPorts: nat.PortSet{"9999/tcp": {}},
1✔
732
                                Healthcheck: &container.HealthConfig{
1✔
733
                                        Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
1✔
734
                                                "http://127.0.0.1:9999/health",
1✔
735
                                        },
1✔
736
                                        Interval: 10 * time.Second,
1✔
737
                                        Timeout:  2 * time.Second,
1✔
738
                                        Retries:  3,
1✔
739
                                },
1✔
740
                        },
1✔
741
                        container.HostConfig{
1✔
742
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
743
                        },
1✔
744
                        network.NetworkingConfig{
1✔
745
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
746
                                        utils.NetId: {
1✔
747
                                                Aliases: utils.GotrueAliases,
1✔
748
                                        },
1✔
749
                                },
1✔
750
                        },
1✔
751
                        utils.GotrueId,
1✔
752
                ); err != nil {
1✔
753
                        return err
×
754
                }
×
755
                started = append(started, utils.GotrueId)
1✔
756
        }
757

758
        // Start Mailpit
759
        if utils.Config.Inbucket.Enabled && !isContainerExcluded(utils.Config.Inbucket.Image, excluded) {
3✔
760
                inbucketPortBindings := nat.PortMap{"8025/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Port), 10)}}}
1✔
761
                if utils.Config.Inbucket.SmtpPort != 0 {
1✔
762
                        inbucketPortBindings["1025/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.SmtpPort), 10)}}
×
763
                }
×
764
                if utils.Config.Inbucket.Pop3Port != 0 {
1✔
765
                        inbucketPortBindings["1110/tcp"] = []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Inbucket.Pop3Port), 10)}}
×
766
                }
×
767
                if _, err := utils.DockerStart(
1✔
768
                        ctx,
1✔
769
                        container.Config{
1✔
770
                                Image: utils.Config.Inbucket.Image,
1✔
771
                                Healthcheck: &container.HealthConfig{
1✔
772
                                        Test:     []string{"CMD", "/mailpit", "readyz"},
1✔
773
                                        Interval: 10 * time.Second,
1✔
774
                                        Timeout:  2 * time.Second,
1✔
775
                                        Retries:  3,
1✔
776
                                        // StartPeriod taken from upstream Dockerfile
1✔
777
                                        StartPeriod: 10 * time.Second,
1✔
778
                                },
1✔
779
                        },
1✔
780
                        container.HostConfig{
1✔
781
                                PortBindings:  inbucketPortBindings,
1✔
782
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
783
                        },
1✔
784
                        network.NetworkingConfig{
1✔
785
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
786
                                        utils.NetId: {
1✔
787
                                                Aliases: utils.InbucketAliases,
1✔
788
                                        },
1✔
789
                                },
1✔
790
                        },
1✔
791
                        utils.InbucketId,
1✔
792
                ); err != nil {
1✔
793
                        return err
×
794
                }
×
795
                started = append(started, utils.InbucketId)
1✔
796
        }
797

798
        // Start Realtime.
799
        if utils.Config.Realtime.Enabled && !isContainerExcluded(utils.Config.Realtime.Image, excluded) {
3✔
800
                if _, err := utils.DockerStart(
1✔
801
                        ctx,
1✔
802
                        container.Config{
1✔
803
                                Image: utils.Config.Realtime.Image,
1✔
804
                                Env: []string{
1✔
805
                                        "PORT=4000",
1✔
806
                                        "DB_HOST=" + dbConfig.Host,
1✔
807
                                        fmt.Sprintf("DB_PORT=%d", dbConfig.Port),
1✔
808
                                        "DB_USER=" + utils.SUPERUSER_ROLE,
1✔
809
                                        "DB_PASSWORD=" + dbConfig.Password,
1✔
810
                                        "DB_NAME=" + dbConfig.Database,
1✔
811
                                        "DB_AFTER_CONNECT_QUERY=SET search_path TO _realtime",
1✔
812
                                        "DB_ENC_KEY=" + utils.Config.Realtime.EncryptionKey,
1✔
813
                                        "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
814
                                        fmt.Sprintf("API_JWT_JWKS=%s", jwks),
1✔
815
                                        "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
816
                                        "APP_NAME=realtime",
1✔
817
                                        "SECRET_KEY_BASE=" + utils.Config.Realtime.SecretKeyBase,
1✔
818
                                        "ERL_AFLAGS=" + utils.ToRealtimeEnv(utils.Config.Realtime.IpVersion),
1✔
819
                                        "DNS_NODES=''",
1✔
820
                                        "RLIMIT_NOFILE=",
1✔
821
                                        "SEED_SELF_HOST=true",
1✔
822
                                        "RUN_JANITOR=true",
1✔
823
                                        fmt.Sprintf("MAX_HEADER_LENGTH=%d", utils.Config.Realtime.MaxHeaderLength),
1✔
824
                                },
1✔
825
                                ExposedPorts: nat.PortSet{"4000/tcp": {}},
1✔
826
                                Healthcheck: &container.HealthConfig{
1✔
827
                                        // Podman splits command by spaces unless it's quoted, but curl header can't be quoted.
1✔
828
                                        Test: []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null",
1✔
829
                                                "-H", "Host:" + utils.Config.Realtime.TenantId,
1✔
830
                                                "http://127.0.0.1:4000/api/ping",
1✔
831
                                        },
1✔
832
                                        Interval: 10 * time.Second,
1✔
833
                                        Timeout:  2 * time.Second,
1✔
834
                                        Retries:  3,
1✔
835
                                },
1✔
836
                        },
1✔
837
                        container.HostConfig{
1✔
838
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
839
                        },
1✔
840
                        network.NetworkingConfig{
1✔
841
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
842
                                        utils.NetId: {
1✔
843
                                                Aliases: utils.RealtimeAliases,
1✔
844
                                        },
1✔
845
                                },
1✔
846
                        },
1✔
847
                        utils.RealtimeId,
1✔
848
                ); err != nil {
1✔
849
                        return err
×
850
                }
×
851
                started = append(started, utils.RealtimeId)
1✔
852
        }
853

854
        // Start PostgREST.
855
        if utils.Config.Api.Enabled && !isContainerExcluded(utils.Config.Api.Image, excluded) {
3✔
856
                if _, err := utils.DockerStart(
1✔
857
                        ctx,
1✔
858
                        container.Config{
1✔
859
                                Image: utils.Config.Api.Image,
1✔
860
                                Env: []string{
1✔
861
                                        fmt.Sprintf("PGRST_DB_URI=postgresql://authenticator:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
1✔
862
                                        "PGRST_DB_SCHEMAS=" + strings.Join(utils.Config.Api.Schemas, ","),
1✔
863
                                        "PGRST_DB_EXTRA_SEARCH_PATH=" + strings.Join(utils.Config.Api.ExtraSearchPath, ","),
1✔
864
                                        fmt.Sprintf("PGRST_DB_MAX_ROWS=%d", utils.Config.Api.MaxRows),
1✔
865
                                        "PGRST_DB_ANON_ROLE=anon",
1✔
866
                                        fmt.Sprintf("PGRST_JWT_SECRET=%s", jwks),
1✔
867
                                        "PGRST_ADMIN_SERVER_PORT=3001",
1✔
868
                                },
1✔
869
                                // PostgREST does not expose a shell for health check
1✔
870
                        },
1✔
871
                        container.HostConfig{
1✔
872
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
873
                        },
1✔
874
                        network.NetworkingConfig{
1✔
875
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
876
                                        utils.NetId: {
1✔
877
                                                Aliases: utils.RestAliases,
1✔
878
                                        },
1✔
879
                                },
1✔
880
                        },
1✔
881
                        utils.RestId,
1✔
882
                ); err != nil {
1✔
883
                        return err
×
884
                }
×
885
                started = append(started, utils.RestId)
1✔
886
        }
887

888
        // Start Storage.
889
        if isStorageEnabled {
3✔
890
                dockerStoragePath := "/mnt"
1✔
891
                if _, err := utils.DockerStart(
1✔
892
                        ctx,
1✔
893
                        container.Config{
1✔
894
                                Image: utils.Config.Storage.Image,
1✔
895
                                Env: []string{
1✔
896
                                        "DB_MIGRATIONS_FREEZE_AT=" + utils.Config.Storage.TargetMigration,
1✔
897
                                        "ANON_KEY=" + utils.Config.Auth.AnonKey.Value,
1✔
898
                                        "SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value,
1✔
899
                                        "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
900
                                        fmt.Sprintf("JWT_JWKS=%s", jwks),
1✔
901
                                        fmt.Sprintf("DATABASE_URL=postgresql://supabase_storage_admin:%s@%s:%d/%s", dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database),
1✔
902
                                        fmt.Sprintf("FILE_SIZE_LIMIT=%v", utils.Config.Storage.FileSizeLimit),
1✔
903
                                        "STORAGE_BACKEND=file",
1✔
904
                                        "FILE_STORAGE_BACKEND_PATH=" + dockerStoragePath,
1✔
905
                                        "TENANT_ID=stub",
1✔
906
                                        // TODO: https://github.com/supabase/storage-api/issues/55
1✔
907
                                        "STORAGE_S3_REGION=" + utils.Config.Storage.S3Credentials.Region,
1✔
908
                                        "GLOBAL_S3_BUCKET=stub",
1✔
909
                                        fmt.Sprintf("ENABLE_IMAGE_TRANSFORMATION=%t", isImgProxyEnabled),
1✔
910
                                        fmt.Sprintf("IMGPROXY_URL=http://%s:5001", utils.ImgProxyId),
1✔
911
                                        "TUS_URL_PATH=/storage/v1/upload/resumable",
1✔
912
                                        "S3_PROTOCOL_ACCESS_KEY_ID=" + utils.Config.Storage.S3Credentials.AccessKeyId,
1✔
913
                                        "S3_PROTOCOL_ACCESS_KEY_SECRET=" + utils.Config.Storage.S3Credentials.SecretAccessKey,
1✔
914
                                        "S3_PROTOCOL_PREFIX=/storage/v1",
1✔
915
                                        "UPLOAD_FILE_SIZE_LIMIT=52428800000",
1✔
916
                                        "UPLOAD_FILE_SIZE_LIMIT_STANDARD=5242880000",
1✔
917
                                        "SIGNED_UPLOAD_URL_EXPIRATION_TIME=7200",
1✔
918
                                },
1✔
919
                                Healthcheck: &container.HealthConfig{
1✔
920
                                        // For some reason, localhost resolves to IPv6 address on GitPod which breaks healthcheck.
1✔
921
                                        Test: []string{"CMD", "wget", "--no-verbose", "--tries=1", "--spider",
1✔
922
                                                "http://127.0.0.1:5000/status",
1✔
923
                                        },
1✔
924
                                        Interval: 10 * time.Second,
1✔
925
                                        Timeout:  2 * time.Second,
1✔
926
                                        Retries:  3,
1✔
927
                                },
1✔
928
                        },
1✔
929
                        container.HostConfig{
1✔
930
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
931
                                Binds:         []string{utils.StorageId + ":" + dockerStoragePath},
1✔
932
                        },
1✔
933
                        network.NetworkingConfig{
1✔
934
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
935
                                        utils.NetId: {
1✔
936
                                                Aliases: utils.StorageAliases,
1✔
937
                                        },
1✔
938
                                },
1✔
939
                        },
1✔
940
                        utils.StorageId,
1✔
941
                ); err != nil {
1✔
942
                        return err
×
943
                }
×
944
                started = append(started, utils.StorageId)
1✔
945
        }
946

947
        // Start Storage ImgProxy.
948
        if isStorageEnabled && isImgProxyEnabled {
2✔
949
                if _, err := utils.DockerStart(
×
950
                        ctx,
×
951
                        container.Config{
×
952
                                Image: utils.Config.Storage.ImgProxyImage,
×
953
                                Env: []string{
×
954
                                        "IMGPROXY_BIND=:5001",
×
955
                                        "IMGPROXY_LOCAL_FILESYSTEM_ROOT=/",
×
956
                                        "IMGPROXY_USE_ETAG=/",
×
957
                                        "IMGPROXY_MAX_SRC_RESOLUTION=50",
×
958
                                        "IMGPROXY_MAX_SRC_FILE_SIZE=25000000",
×
959
                                        "IMGPROXY_MAX_ANIMATION_FRAMES=60",
×
960
                                        "IMGPROXY_ENABLE_WEBP_DETECTION=true",
×
961
                                        "IMGPROXY_PRESETS=default=width:3000/height:8192",
×
962
                                        "IMGPROXY_FORMAT_QUALITY=jpeg=80,avif=62,webp=80",
×
963
                                },
×
964
                                Healthcheck: &container.HealthConfig{
×
965
                                        Test:     []string{"CMD", "imgproxy", "health"},
×
966
                                        Interval: 10 * time.Second,
×
967
                                        Timeout:  2 * time.Second,
×
968
                                        Retries:  3,
×
969
                                },
×
970
                        },
×
971
                        container.HostConfig{
×
972
                                VolumesFrom:   []string{utils.StorageId},
×
973
                                RestartPolicy: container.RestartPolicy{Name: "always"},
×
974
                        },
×
975
                        network.NetworkingConfig{
×
976
                                EndpointsConfig: map[string]*network.EndpointSettings{
×
977
                                        utils.NetId: {
×
978
                                                Aliases: utils.ImgProxyAliases,
×
979
                                        },
×
980
                                },
×
981
                        },
×
982
                        utils.ImgProxyId,
×
983
                ); err != nil {
×
984
                        return err
×
985
                }
×
986
                started = append(started, utils.ImgProxyId)
×
987
        }
988

989
        // Start all functions.
990
        if utils.Config.EdgeRuntime.Enabled && !isContainerExcluded(utils.Config.EdgeRuntime.Image, excluded) {
3✔
991
                dbUrl := fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, dbConfig.Database)
1✔
992
                if err := serve.ServeFunctions(ctx, "", nil, "", dbUrl, serve.RuntimeOption{}, fsys); err != nil {
1✔
993
                        return err
×
994
                }
×
995
                started = append(started, utils.EdgeRuntimeId)
1✔
996
        }
997

998
        // Start pg-meta.
999
        if utils.Config.Studio.Enabled && !isContainerExcluded(utils.Config.Studio.PgmetaImage, excluded) {
3✔
1000
                if _, err := utils.DockerStart(
1✔
1001
                        ctx,
1✔
1002
                        container.Config{
1✔
1003
                                Image: utils.Config.Studio.PgmetaImage,
1✔
1004
                                Env: []string{
1✔
1005
                                        "PG_META_PORT=8080",
1✔
1006
                                        "PG_META_DB_HOST=" + dbConfig.Host,
1✔
1007
                                        "PG_META_DB_NAME=" + dbConfig.Database,
1✔
1008
                                        "PG_META_DB_USER=" + dbConfig.User,
1✔
1009
                                        fmt.Sprintf("PG_META_DB_PORT=%d", dbConfig.Port),
1✔
1010
                                        "PG_META_DB_PASSWORD=" + dbConfig.Password,
1✔
1011
                                },
1✔
1012
                                Healthcheck: &container.HealthConfig{
1✔
1013
                                        Test:     []string{"CMD-SHELL", `node --eval="fetch('http://127.0.0.1:8080/health').then((r) => {if (!r.ok) throw new Error(r.status)})"`},
1✔
1014
                                        Interval: 10 * time.Second,
1✔
1015
                                        Timeout:  2 * time.Second,
1✔
1016
                                        Retries:  3,
1✔
1017
                                },
1✔
1018
                        },
1✔
1019
                        container.HostConfig{
1✔
1020
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1021
                        },
1✔
1022
                        network.NetworkingConfig{
1✔
1023
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1024
                                        utils.NetId: {
1✔
1025
                                                Aliases: utils.PgmetaAliases,
1✔
1026
                                        },
1✔
1027
                                },
1✔
1028
                        },
1✔
1029
                        utils.PgmetaId,
1✔
1030
                ); err != nil {
1✔
1031
                        return err
×
1032
                }
×
1033
                started = append(started, utils.PgmetaId)
1✔
1034
        }
1035

1036
        // Start Studio.
1037
        if utils.Config.Studio.Enabled && !isContainerExcluded(utils.Config.Studio.Image, excluded) {
3✔
1038
                if _, err := utils.DockerStart(
1✔
1039
                        ctx,
1✔
1040
                        container.Config{
1✔
1041
                                Image: utils.Config.Studio.Image,
1✔
1042
                                Env: []string{
1✔
1043
                                        "CURRENT_CLI_VERSION=" + utils.Version,
1✔
1044
                                        "STUDIO_PG_META_URL=http://" + utils.PgmetaId + ":8080",
1✔
1045
                                        "POSTGRES_PASSWORD=" + dbConfig.Password,
1✔
1046
                                        "SUPABASE_URL=http://" + utils.KongId + ":8000",
1✔
1047
                                        "SUPABASE_PUBLIC_URL=" + utils.Config.Studio.ApiUrl,
1✔
1048
                                        "AUTH_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
1✔
1049
                                        "SUPABASE_ANON_KEY=" + utils.Config.Auth.AnonKey.Value,
1✔
1050
                                        "SUPABASE_SERVICE_KEY=" + utils.Config.Auth.ServiceRoleKey.Value,
1✔
1051
                                        "LOGFLARE_PRIVATE_ACCESS_TOKEN=" + utils.Config.Analytics.ApiKey,
1✔
1052
                                        "OPENAI_API_KEY=" + utils.Config.Studio.OpenaiApiKey.Value,
1✔
1053
                                        fmt.Sprintf("LOGFLARE_URL=http://%v:4000", utils.LogflareId),
1✔
1054
                                        fmt.Sprintf("NEXT_PUBLIC_ENABLE_LOGS=%v", utils.Config.Analytics.Enabled),
1✔
1055
                                        fmt.Sprintf("NEXT_ANALYTICS_BACKEND_PROVIDER=%v", utils.Config.Analytics.Backend),
1✔
1056
                                        // Ref: https://github.com/vercel/next.js/issues/51684#issuecomment-1612834913
1✔
1057
                                        "HOSTNAME=0.0.0.0",
1✔
1058
                                },
1✔
1059
                                Healthcheck: &container.HealthConfig{
1✔
1060
                                        Test:     []string{"CMD-SHELL", `node --eval="fetch('http://127.0.0.1:3000/api/platform/profile').then((r) => {if (!r.ok) throw new Error(r.status)})"`},
1✔
1061
                                        Interval: 10 * time.Second,
1✔
1062
                                        Timeout:  2 * time.Second,
1✔
1063
                                        Retries:  3,
1✔
1064
                                },
1✔
1065
                        },
1✔
1066
                        container.HostConfig{
1✔
1067
                                PortBindings:  nat.PortMap{"3000/tcp": []nat.PortBinding{{HostPort: strconv.FormatUint(uint64(utils.Config.Studio.Port), 10)}}},
1✔
1068
                                RestartPolicy: container.RestartPolicy{Name: "always"},
1✔
1069
                        },
1✔
1070
                        network.NetworkingConfig{
1✔
1071
                                EndpointsConfig: map[string]*network.EndpointSettings{
1✔
1072
                                        utils.NetId: {
1✔
1073
                                                Aliases: utils.StudioAliases,
1✔
1074
                                        },
1✔
1075
                                },
1✔
1076
                        },
1✔
1077
                        utils.StudioId,
1✔
1078
                ); err != nil {
1✔
1079
                        return err
×
1080
                }
×
1081
                started = append(started, utils.StudioId)
1✔
1082
        }
1083

1084
        // Start pooler.
1085
        if utils.Config.Db.Pooler.Enabled && !isContainerExcluded(utils.Config.Db.Pooler.Image, excluded) {
2✔
1086
                portSession := uint16(5432)
×
1087
                portTransaction := uint16(6543)
×
1088
                dockerPort := portTransaction
×
1089
                if utils.Config.Db.Pooler.PoolMode == config.SessionMode {
×
1090
                        dockerPort = portSession
×
1091
                }
×
1092
                // Create pooler tenant
1093
                var poolerTenantBuf bytes.Buffer
×
1094
                if err := poolerTenantTemplate.Option("missingkey=error").Execute(&poolerTenantBuf, poolerTenant{
×
1095
                        DbHost:            dbConfig.Host,
×
1096
                        DbPort:            dbConfig.Port,
×
1097
                        DbDatabase:        dbConfig.Database,
×
1098
                        DbPassword:        dbConfig.Password,
×
1099
                        ExternalId:        utils.Config.Db.Pooler.TenantId,
×
1100
                        ModeType:          utils.Config.Db.Pooler.PoolMode,
×
1101
                        DefaultMaxClients: utils.Config.Db.Pooler.MaxClientConn,
×
1102
                        DefaultPoolSize:   utils.Config.Db.Pooler.DefaultPoolSize,
×
1103
                }); err != nil {
×
1104
                        return errors.Errorf("failed to exec template: %w", err)
×
1105
                }
×
1106
                if _, err := utils.DockerStart(
×
1107
                        ctx,
×
1108
                        container.Config{
×
1109
                                Image: utils.Config.Db.Pooler.Image,
×
1110
                                Env: []string{
×
1111
                                        "PORT=4000",
×
1112
                                        fmt.Sprintf("PROXY_PORT_SESSION=%d", portSession),
×
1113
                                        fmt.Sprintf("PROXY_PORT_TRANSACTION=%d", portTransaction),
×
1114
                                        fmt.Sprintf("DATABASE_URL=ecto://%s:%s@%s:%d/%s", dbConfig.User, dbConfig.Password, dbConfig.Host, dbConfig.Port, "_supabase"),
×
1115
                                        "CLUSTER_POSTGRES=true",
×
1116
                                        "SECRET_KEY_BASE=" + utils.Config.Db.Pooler.SecretKeyBase,
×
1117
                                        "VAULT_ENC_KEY=" + utils.Config.Db.Pooler.EncryptionKey,
×
1118
                                        "API_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
×
1119
                                        "METRICS_JWT_SECRET=" + utils.Config.Auth.JwtSecret.Value,
×
1120
                                        "REGION=local",
×
1121
                                        "RUN_JANITOR=true",
×
1122
                                        "ERL_AFLAGS=-proto_dist inet_tcp",
×
1123
                                },
×
1124
                                Cmd: []string{
×
1125
                                        "/bin/sh", "-c",
×
1126
                                        fmt.Sprintf("/app/bin/migrate && /app/bin/supavisor eval '%s' && /app/bin/server", poolerTenantBuf.String()),
×
1127
                                },
×
1128
                                ExposedPorts: nat.PortSet{
×
1129
                                        "4000/tcp": {},
×
1130
                                        nat.Port(fmt.Sprintf("%d/tcp", portSession)):     {},
×
1131
                                        nat.Port(fmt.Sprintf("%d/tcp", portTransaction)): {},
×
1132
                                },
×
1133
                                Healthcheck: &container.HealthConfig{
×
1134
                                        Test:     []string{"CMD", "curl", "-sSfL", "--head", "-o", "/dev/null", "http://127.0.0.1:4000/api/health"},
×
1135
                                        Interval: 10 * time.Second,
×
1136
                                        Timeout:  2 * time.Second,
×
1137
                                        Retries:  3,
×
1138
                                },
×
1139
                        },
×
1140
                        container.HostConfig{
×
1141
                                PortBindings: nat.PortMap{nat.Port(fmt.Sprintf("%d/tcp", dockerPort)): []nat.PortBinding{{
×
1142
                                        HostPort: strconv.FormatUint(uint64(utils.Config.Db.Pooler.Port), 10)},
×
1143
                                }},
×
1144
                                RestartPolicy: container.RestartPolicy{Name: "always"},
×
1145
                        },
×
1146
                        network.NetworkingConfig{
×
1147
                                EndpointsConfig: map[string]*network.EndpointSettings{
×
1148
                                        utils.NetId: {
×
1149
                                                Aliases: utils.PoolerAliases,
×
1150
                                        },
×
1151
                                },
×
1152
                        },
×
1153
                        utils.PoolerId,
×
1154
                ); err != nil {
×
1155
                        return err
×
1156
                }
×
1157
                started = append(started, utils.PoolerId)
×
1158
        }
1159

1160
        fmt.Fprintln(os.Stderr, "Waiting for health checks...")
2✔
1161
        if utils.NoBackupVolume && slices.Contains(started, utils.StorageId) {
3✔
1162
                if err := start.WaitForHealthyService(ctx, serviceTimeout, utils.StorageId); err != nil {
1✔
1163
                        return err
×
1164
                }
×
1165
                // Disable prompts when seeding
1166
                if err := buckets.Run(ctx, "", false, fsys); err != nil {
1✔
1167
                        return err
×
1168
                }
×
1169
        }
1170
        return start.WaitForHealthyService(ctx, serviceTimeout, started...)
2✔
1171
}
1172

1173
func isContainerExcluded(imageName string, excluded map[string]bool) bool {
22✔
1174
        short := utils.ShortContainerImageName(imageName)
22✔
1175
        val, ok := excluded[short]
22✔
1176
        return ok && val
22✔
1177
}
22✔
1178

1179
func ExcludableContainers() []string {
1✔
1180
        names := []string{}
1✔
1181
        for _, image := range config.Images.Services() {
14✔
1182
                names = append(names, utils.ShortContainerImageName(image))
13✔
1183
        }
13✔
1184
        return names
1✔
1185
}
1186

1187
func formatMapForEnvConfig(input map[string]string, output *bytes.Buffer) {
5✔
1188
        numOfKeyPairs := len(input)
5✔
1189
        i := 0
5✔
1190
        for k, v := range input {
15✔
1191
                output.WriteString(k)
10✔
1192
                output.WriteString(":")
10✔
1193
                output.WriteString(v)
10✔
1194
                i++
10✔
1195
                if i < numOfKeyPairs {
16✔
1196
                        output.WriteString(",")
6✔
1197
                }
6✔
1198
        }
1199
}
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