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

supabase / cli / 19032133102

03 Nov 2025 10:55AM UTC coverage: 54.585% (-0.1%) from 54.699%
19032133102

Pull #4394

github

web-flow
Merge 3b6ecd499 into df557dc33
Pull Request #4394: feat(cli): add concurrent images pulling

213 of 384 new or added lines in 3 files covered. (55.47%)

16 existing lines in 2 files now uncovered.

6584 of 12062 relevant lines covered (54.58%)

6.36 hits per line

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

62.55
/internal/utils/docker.go
1
package utils
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/base64"
7
        "encoding/json"
8
        "fmt"
9
        "io"
10
        "log"
11
        "os"
12
        "regexp"
13
        "strings"
14
        "sync"
15
        "time"
16

17
        "github.com/containerd/errdefs"
18
        podman "github.com/containers/common/libnetwork/types"
19
        "github.com/docker/cli/cli/command"
20
        "github.com/docker/cli/cli/compose/loader"
21
        dockerConfig "github.com/docker/cli/cli/config"
22
        dockerFlags "github.com/docker/cli/cli/flags"
23
        "github.com/docker/cli/cli/streams"
24
        "github.com/docker/docker/api/types/container"
25
        "github.com/docker/docker/api/types/filters"
26
        "github.com/docker/docker/api/types/image"
27
        "github.com/docker/docker/api/types/mount"
28
        "github.com/docker/docker/api/types/network"
29
        "github.com/docker/docker/api/types/versions"
30
        "github.com/docker/docker/api/types/volume"
31
        "github.com/docker/docker/client"
32
        "github.com/docker/docker/pkg/jsonmessage"
33
        "github.com/docker/docker/pkg/stdcopy"
34
        "github.com/go-errors/errors"
35
        "github.com/spf13/viper"
36
        "go.opentelemetry.io/otel"
37

38
        "github.com/docker/compose/v2/pkg/progress"
39
)
40

41
var Docker = NewDocker()
42

43
func NewDocker() *client.Client {
87✔
44
        // TODO: refactor to initialize lazily
87✔
45
        cli, err := command.NewDockerCli()
87✔
46
        if err != nil {
87✔
47
                log.Fatalln("Failed to create Docker client:", err)
×
48
        }
×
49
        // Silence otel errors as users don't care about docker metrics
50
        // 2024/08/12 23:11:12 1 errors occurred detecting resource:
51
        //         * conflicting Schema URL: https://opentelemetry.io/schemas/1.21.0
52
        otel.SetErrorHandler(otel.ErrorHandlerFunc(func(cause error) {}))
87✔
53
        if err := cli.Initialize(&dockerFlags.ClientOptions{}); err != nil {
87✔
54
                log.Fatalln("Failed to initialize Docker client:", err)
×
55
        }
×
56
        return cli.Client().(*client.Client)
87✔
57
}
58

59
const (
60
        DinDHost            = "host.docker.internal"
61
        CliProjectLabel     = "com.supabase.cli.project"
62
        composeProjectLabel = "com.docker.compose.project"
63
)
64

65
func DockerNetworkCreateIfNotExists(ctx context.Context, mode container.NetworkMode, labels map[string]string) error {
73✔
66
        // Non-user defined networks should already exist
73✔
67
        if !isUserDefined(mode) {
85✔
68
                return nil
12✔
69
        }
12✔
70
        _, err := Docker.NetworkCreate(ctx, mode.NetworkName(), network.CreateOptions{Labels: labels})
61✔
71
        // if error is network already exists, no need to propagate to user
61✔
72
        if errdefs.IsConflict(err) || errors.Is(err, podman.ErrNetworkExists) {
61✔
73
                return nil
×
74
        }
×
75
        if err != nil {
61✔
76
                return errors.Errorf("failed to create docker network: %w", err)
×
77
        }
×
78
        return err
61✔
79
}
80

81
func WaitAll[T any](containers []T, exec func(container T) error) []error {
10✔
82
        var wg sync.WaitGroup
10✔
83
        result := make([]error, len(containers))
10✔
84
        for i, container := range containers {
23✔
85
                wg.Add(1)
13✔
86
                go func(i int, container T) {
26✔
87
                        defer wg.Done()
13✔
88
                        result[i] = exec(container)
13✔
89
                }(i, container)
13✔
90
        }
91
        wg.Wait()
10✔
92
        return result
10✔
93
}
94

95
// NoBackupVolume TODO: encapsulate this state in a class
96
var NoBackupVolume = false
97

98
func DockerRemoveAll(ctx context.Context, w io.Writer, projectId string) error {
8✔
99
        fmt.Fprintln(w, "Stopping containers...")
8✔
100
        args := CliProjectFilter(projectId)
8✔
101
        containers, err := Docker.ContainerList(ctx, container.ListOptions{
8✔
102
                All:     true,
8✔
103
                Filters: args,
8✔
104
        })
8✔
105
        if err != nil {
9✔
106
                return errors.Errorf("failed to list containers: %w", err)
1✔
107
        }
1✔
108
        // Gracefully shutdown containers
109
        var ids []string
7✔
110
        for _, c := range containers {
9✔
111
                if c.State == "running" {
3✔
112
                        ids = append(ids, c.ID)
1✔
113
                }
1✔
114
        }
115
        result := WaitAll(ids, func(id string) error {
8✔
116
                if err := Docker.ContainerStop(ctx, id, container.StopOptions{}); err != nil {
1✔
117
                        return errors.Errorf("failed to stop container: %w", err)
×
118
                }
×
119
                return nil
1✔
120
        })
121
        if err := errors.Join(result...); err != nil {
7✔
122
                return err
×
123
        }
×
124
        if report, err := Docker.ContainersPrune(ctx, args); err != nil {
8✔
125
                return errors.Errorf("failed to prune containers: %w", err)
1✔
126
        } else if viper.GetBool("DEBUG") {
7✔
127
                fmt.Fprintln(os.Stderr, "Pruned containers:", report.ContainersDeleted)
×
128
        }
×
129
        // Remove named volumes
130
        if NoBackupVolume {
8✔
131
                vargs := args.Clone()
2✔
132
                if versions.GreaterThanOrEqualTo(Docker.ClientVersion(), "1.42") {
3✔
133
                        // Since docker engine 25.0.3, all flag is required to include named volumes.
1✔
134
                        // https://github.com/docker/cli/blob/master/cli/command/volume/prune.go#L76
1✔
135
                        vargs.Add("all", "true")
1✔
136
                }
1✔
137
                if report, err := Docker.VolumesPrune(ctx, vargs); err != nil {
2✔
138
                        return errors.Errorf("failed to prune volumes: %w", err)
×
139
                } else if viper.GetBool("DEBUG") {
2✔
140
                        fmt.Fprintln(os.Stderr, "Pruned volumes:", report.VolumesDeleted)
×
141
                }
×
142
        }
143
        // Remove networks.
144
        if report, err := Docker.NetworksPrune(ctx, args); err != nil {
6✔
145
                return errors.Errorf("failed to prune networks: %w", err)
×
146
        } else if viper.GetBool("DEBUG") {
6✔
147
                fmt.Fprintln(os.Stderr, "Pruned network:", report.NetworksDeleted)
×
148
        }
×
149
        return nil
6✔
150
}
151

152
func CliProjectFilter(projectId string) filters.Args {
15✔
153
        if len(projectId) == 0 {
17✔
154
                return filters.NewArgs(
2✔
155
                        filters.Arg("label", CliProjectLabel),
2✔
156
                )
2✔
157
        }
2✔
158
        return filters.NewArgs(
13✔
159
                filters.Arg("label", CliProjectLabel+"="+projectId),
13✔
160
        )
13✔
161
}
162

163
var (
164
        // Only supports one registry per command invocation
165
        registryAuth string
166
        registryOnce sync.Once
167
)
168

169
func GetRegistryAuth() string {
4✔
170
        registryOnce.Do(func() {
5✔
171
                config := dockerConfig.LoadDefaultConfigFile(os.Stderr)
1✔
172
                // Ref: https://docs.docker.com/engine/api/sdk/examples/#pull-an-image-with-authentication
1✔
173
                auth, err := config.GetAuthConfig(GetRegistry())
1✔
174
                if err != nil {
1✔
175
                        fmt.Fprintln(os.Stderr, "Failed to load registry credentials:", err)
×
176
                        return
×
177
                }
×
178
                encoded, err := json.Marshal(auth)
1✔
179
                if err != nil {
1✔
180
                        fmt.Fprintln(os.Stderr, "Failed to serialise auth config:", err)
×
181
                        return
×
182
                }
×
183
                registryAuth = base64.URLEncoding.EncodeToString(encoded)
1✔
184
        })
185
        return registryAuth
4✔
186
}
187

188
// Defaults to Supabase public ECR for faster image pull
189
const defaultRegistry = "public.ecr.aws"
190

191
func GetRegistry() string {
264✔
192
        registry := viper.GetString("INTERNAL_IMAGE_REGISTRY")
264✔
193
        if len(registry) == 0 {
509✔
194
                return defaultRegistry
245✔
195
        }
245✔
196
        return strings.ToLower(registry)
19✔
197
}
198

199
func GetRegistryImageUrl(imageName string) string {
263✔
200
        registry := GetRegistry()
263✔
201
        if registry == "docker.io" {
281✔
202
                return imageName
18✔
203
        }
18✔
204
        // Configure mirror registry
205
        parts := strings.Split(imageName, "/")
245✔
206
        imageName = parts[len(parts)-1]
245✔
207
        return registry + "/supabase/" + imageName
245✔
208
}
209

210
func DockerImagePull(ctx context.Context, imageTag string, w io.Writer) error {
4✔
211
        out, err := Docker.ImagePull(ctx, imageTag, image.PullOptions{
4✔
212
                RegistryAuth: GetRegistryAuth(),
4✔
213
        })
4✔
214
        if err != nil {
5✔
215
                return errors.Errorf("failed to pull docker image: %w", err)
1✔
216
        }
1✔
217
        defer out.Close()
3✔
218
        if err := jsonmessage.DisplayJSONMessagesToStream(out, streams.NewOut(w), nil); err != nil {
5✔
219
                return errors.Errorf("failed to display json stream: %w", err)
2✔
220
        }
2✔
221
        return nil
1✔
222
}
223

224
// DockerImagePullWithProgress pulls an image and reports progress using docker-compose style progress events.
225
// The progress writer is used to emit progress events for each layer being pulled.
NEW
226
func DockerImagePullWithProgress(ctx context.Context, imageUrl string, resource string, writer progress.Writer) error {
×
NEW
227
        stream, err := Docker.ImagePull(ctx, imageUrl, image.PullOptions{
×
NEW
228
                RegistryAuth: GetRegistryAuth(),
×
NEW
229
        })
×
NEW
230
        if err != nil {
×
NEW
231
                return errors.Errorf("failed to pull docker image: %w", err)
×
NEW
232
        }
×
233

234
        // Parse and report progress (same as docker-compose)
NEW
235
        dec := json.NewDecoder(stream)
×
NEW
236
        for {
×
NEW
237
                var jm jsonmessage.JSONMessage
×
NEW
238
                if err := dec.Decode(&jm); err != nil {
×
NEW
239
                        if err == io.EOF {
×
NEW
240
                                break
×
241
                        }
NEW
242
                        stream.Close()
×
NEW
243
                        return err
×
244
                }
NEW
245
                if jm.Error != nil {
×
NEW
246
                        stream.Close()
×
NEW
247
                        return errors.New(jm.Error.Message)
×
NEW
248
                }
×
249
                // Convert Docker JSON message to progress event (same logic as docker-compose)
NEW
250
                ToPullProgressEvent(resource, jm, writer)
×
251
        }
NEW
252
        stream.Close()
×
NEW
253
        return nil
×
254
}
255

256
// ToPullProgressEvent converts Docker JSON messages to progress events (same as docker-compose).
257
// This function is used to provide consistent progress reporting across the CLI.
NEW
258
func ToPullProgressEvent(parent string, jm jsonmessage.JSONMessage, writer progress.Writer) {
×
NEW
259
        if jm.ID == "" || jm.Progress == nil {
×
NEW
260
                return
×
NEW
261
        }
×
262

NEW
263
        const (
×
NEW
264
                PreparingPhase         = "Preparing"
×
NEW
265
                WaitingPhase           = "Waiting"
×
NEW
266
                PullingFsPhase         = "Pulling fs layer"
×
NEW
267
                DownloadingPhase       = "Downloading"
×
NEW
268
                DownloadCompletePhase  = "Download complete"
×
NEW
269
                ExtractingPhase        = "Extracting"
×
NEW
270
                VerifyingChecksumPhase = "Verifying Checksum"
×
NEW
271
                AlreadyExistsPhase     = "Already exists"
×
NEW
272
                PullCompletePhase      = "Pull complete"
×
NEW
273
        )
×
NEW
274

×
NEW
275
        var (
×
NEW
276
                text    string
×
NEW
277
                total   int64
×
NEW
278
                percent int
×
NEW
279
                current int64
×
NEW
280
                status  = progress.Working
×
NEW
281
        )
×
NEW
282

×
NEW
283
        text = jm.Progress.String()
×
NEW
284

×
NEW
285
        switch jm.Status {
×
NEW
286
        case PreparingPhase, WaitingPhase, PullingFsPhase:
×
NEW
287
                percent = 0
×
NEW
288
        case DownloadingPhase, ExtractingPhase, VerifyingChecksumPhase:
×
NEW
289
                if jm.Progress != nil {
×
NEW
290
                        current = jm.Progress.Current
×
NEW
291
                        total = jm.Progress.Total
×
NEW
292
                        if jm.Progress.Total > 0 {
×
NEW
293
                                percent = int(jm.Progress.Current * 100 / jm.Progress.Total)
×
NEW
294
                        }
×
295
                }
NEW
296
        case DownloadCompletePhase, AlreadyExistsPhase, PullCompletePhase:
×
NEW
297
                status = progress.Done
×
NEW
298
                percent = 100
×
299
        }
300

NEW
301
        if strings.Contains(jm.Status, "Image is up to date") ||
×
NEW
302
                strings.Contains(jm.Status, "Downloaded newer image") {
×
NEW
303
                status = progress.Done
×
NEW
304
                percent = 100
×
NEW
305
        }
×
306

NEW
307
        if jm.Error != nil {
×
NEW
308
                status = progress.Error
×
NEW
309
                text = jm.Error.Message
×
NEW
310
        }
×
311

NEW
312
        writer.Event(progress.Event{
×
NEW
313
                ID:         jm.ID,
×
NEW
314
                ParentID:   parent,
×
NEW
315
                Current:    current,
×
NEW
316
                Total:      total,
×
NEW
317
                Percent:    percent,
×
NEW
318
                Text:       jm.Status,
×
NEW
319
                Status:     status,
×
NEW
320
                StatusText: text,
×
NEW
321
        })
×
322
}
323

324
// Used by unit tests
325
var timeUnit = time.Second
326

327
// IsRetryablePullError checks if an error is retryable.
328
// Only transient errors like rate limits and network issues should be retried.
329
func IsRetryablePullError(err error) bool {
3✔
330
        if err == nil {
3✔
NEW
331
                return false
×
NEW
332
        }
×
333
        errStr := err.Error()
3✔
334
        // Check for retryable error messages that indicate transient failures
3✔
335
        retryableErrors := []string{
3✔
336
                "toomanyrequests",
3✔
337
                "too many requests",
3✔
338
                "rate limit",
3✔
339
                "service unavailable",
3✔
340
                "503",
3✔
341
                "temporary failure",
3✔
342
                "connection refused",
3✔
343
                "timeout",
3✔
344
        }
3✔
345
        errStrLower := strings.ToLower(errStr)
3✔
346
        for _, retryableErr := range retryableErrors {
16✔
347
                if strings.Contains(errStrLower, retryableErr) {
15✔
348
                        return true
2✔
349
                }
2✔
350
        }
351
        return false
1✔
352
}
353

UNCOV
354
func DockerImagePullWithRetry(ctx context.Context, image string, retries int) error {
×
UNCOV
355
        err := DockerImagePull(ctx, image, os.Stderr)
×
UNCOV
356
        for i := range retries {
×
UNCOV
357
                if err == nil || errors.Is(ctx.Err(), context.Canceled) {
×
UNCOV
358
                        break
×
359
                }
UNCOV
360
                fmt.Fprintln(os.Stderr, err)
×
UNCOV
361
                period := time.Duration(2<<(i+1)) * timeUnit
×
UNCOV
362
                fmt.Fprintf(os.Stderr, "Retrying after %v: %s\n", period, image)
×
UNCOV
363
                time.Sleep(period)
×
UNCOV
364
                err = DockerImagePull(ctx, image, os.Stderr)
×
365
        }
UNCOV
366
        return err
×
367
}
368

369
func DockerPullImageIfNotCached(ctx context.Context, imageName string) error {
89✔
370
        return DockerPullImageIfNotCachedWithWriter(ctx, imageName, os.Stderr, uint(5))
89✔
371
}
89✔
372

373
// DockerPullImageIfNotCachedWithWriter pulls an image if not cached, using the provided writer for output.
374
// Use io.Discard to suppress output.
375
func DockerPullImageIfNotCachedWithWriter(ctx context.Context, imageName string, w io.Writer, retries uint) error {
89✔
376
        imageUrl := GetRegistryImageUrl(imageName)
89✔
377
        if _, err := Docker.ImageInspect(ctx, imageUrl); err == nil {
163✔
378
                return nil
74✔
379
        } else if !errdefs.IsNotFound(err) {
102✔
380
                return errors.Errorf("failed to inspect docker image: %w", err)
13✔
381
        }
13✔
382
        // Pull with retry using the provided writer
383
        // Increased retries to 5 to handle rate limiting better
384
        var onRetry func(uint, time.Duration)
2✔
385
        if w != io.Discard {
4✔
386
                onRetry = func(attempt uint, backoff time.Duration) {
4✔
387
                        fmt.Fprintf(w, "Retrying after %v: %s\n", backoff, imageUrl)
2✔
388
                }
2✔
389
        }
390
        return RetryWithExponentialBackoff(ctx, func() error {
6✔
391
                err := DockerImagePull(ctx, imageUrl, w)
4✔
392
                if err != nil && w != io.Discard {
7✔
393
                        fmt.Fprintln(w, err)
3✔
394
                }
3✔
395
                return err
4✔
396
        }, retries, onRetry, IsRetryablePullError)
397
}
398

399
var suggestDockerInstall = "Docker Desktop is a prerequisite for local development. Follow the official docs to install: https://docs.docker.com/desktop"
400

401
func DockerStart(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string) (string, error) {
85✔
402
        // Pull container image
85✔
403
        if err := DockerPullImageIfNotCached(ctx, config.Image); err != nil {
97✔
404
                if client.IsErrConnectionFailed(err) {
22✔
405
                        CmdSuggestion = suggestDockerInstall
10✔
406
                }
10✔
407
                return "", err
12✔
408
        }
409
        // Setup default config
410
        config.Image = GetRegistryImageUrl(config.Image)
73✔
411
        if config.Labels == nil {
146✔
412
                config.Labels = make(map[string]string, 2)
73✔
413
        }
73✔
414
        config.Labels[CliProjectLabel] = Config.ProjectId
73✔
415
        config.Labels[composeProjectLabel] = Config.ProjectId
73✔
416
        // Configure container network
73✔
417
        hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, extraHosts...)
73✔
418
        if networkId := viper.GetString("network-id"); len(networkId) > 0 {
73✔
419
                hostConfig.NetworkMode = container.NetworkMode(networkId)
×
420
        } else if len(hostConfig.NetworkMode) == 0 {
134✔
421
                hostConfig.NetworkMode = container.NetworkMode(NetId)
61✔
422
        }
61✔
423
        if err := DockerNetworkCreateIfNotExists(ctx, hostConfig.NetworkMode, config.Labels); err != nil {
73✔
424
                return "", err
×
425
        }
×
426
        // Configure container volumes
427
        var binds, sources []string
73✔
428
        for _, bind := range hostConfig.Binds {
116✔
429
                spec, err := loader.ParseVolume(bind)
43✔
430
                if err != nil {
43✔
431
                        return "", errors.Errorf("failed to parse docker volume: %w", err)
×
432
                }
×
433
                if spec.Type != string(mount.TypeVolume) {
62✔
434
                        binds = append(binds, bind)
19✔
435
                } else if len(spec.Source) > 0 {
67✔
436
                        sources = append(sources, spec.Source)
24✔
437
                }
24✔
438
        }
439
        // Skip named volume for BitBucket pipeline
440
        if os.Getenv("BITBUCKET_CLONE_DIR") != "" {
73✔
441
                hostConfig.Binds = binds
×
442
                // Bitbucket doesn't allow for --security-opt option to be set
×
443
                // https://support.atlassian.com/bitbucket-cloud/docs/run-docker-commands-in-bitbucket-pipelines/#Full-list-of-restricted-commands
×
444
                hostConfig.SecurityOpt = nil
×
445
        } else {
73✔
446
                // Create named volumes with labels
73✔
447
                for _, name := range sources {
97✔
448
                        if _, err := Docker.VolumeCreate(ctx, volume.CreateOptions{
24✔
449
                                Name:   name,
24✔
450
                                Labels: config.Labels,
24✔
451
                        }); err != nil {
24✔
452
                                return "", errors.Errorf("failed to create volume: %w", err)
×
453
                        }
×
454
                }
455
        }
456
        // Create container from image
457
        resp, err := Docker.ContainerCreate(ctx, &config, &hostConfig, &networkingConfig, nil, containerName)
73✔
458
        if err != nil {
74✔
459
                return "", errors.Errorf("failed to create docker container: %w", err)
1✔
460
        }
1✔
461
        // Run container in background
462
        err = Docker.ContainerStart(ctx, resp.ID, container.StartOptions{})
72✔
463
        if err != nil {
73✔
464
                if hostPort := parsePortBindError(err); len(hostPort) > 0 {
1✔
465
                        CmdSuggestion = suggestDockerStop(ctx, hostPort)
×
466
                        prefix := "Or configure"
×
467
                        if len(CmdSuggestion) == 0 {
×
468
                                prefix = "Try configuring"
×
469
                        }
×
470
                        name := containerName
×
471
                        if endpoint, ok := networkingConfig.EndpointsConfig[NetId]; ok && len(endpoint.Aliases) > 0 {
×
472
                                name = endpoint.Aliases[0]
×
473
                        }
×
474
                        CmdSuggestion += fmt.Sprintf("\n%s a different %s port in %s", prefix, name, Bold(ConfigPath))
×
475
                }
476
                err = errors.Errorf("failed to start docker container: %w", err)
1✔
477
        }
478
        return resp.ID, err
72✔
479
}
480

481
func DockerRemove(containerId string) {
52✔
482
        if err := Docker.ContainerRemove(context.Background(), containerId, container.RemoveOptions{
52✔
483
                RemoveVolumes: true,
52✔
484
                Force:         true,
52✔
485
        }); err != nil {
52✔
486
                fmt.Fprintln(os.Stderr, "Failed to remove container:", containerId, err)
×
487
        }
×
488
}
489

490
type DockerJob struct {
491
        Image string
492
        Env   []string
493
        Cmd   []string
494
}
495

496
func DockerRunJob(ctx context.Context, job DockerJob, stdout, stderr io.Writer) error {
20✔
497
        return DockerRunOnceWithStream(ctx, job.Image, job.Env, job.Cmd, stdout, stderr)
20✔
498
}
20✔
499

500
// Runs a container image exactly once, returning stdout and throwing error on non-zero exit code.
501
func DockerRunOnce(ctx context.Context, image string, env []string, cmd []string) (string, error) {
7✔
502
        stderr := GetDebugLogger()
7✔
503
        var out bytes.Buffer
7✔
504
        err := DockerRunOnceWithStream(ctx, image, env, cmd, &out, stderr)
7✔
505
        return out.String(), err
7✔
506
}
7✔
507

508
func DockerRunOnceWithStream(ctx context.Context, image string, env, cmd []string, stdout, stderr io.Writer) error {
27✔
509
        return DockerRunOnceWithConfig(ctx, container.Config{
27✔
510
                Image: image,
27✔
511
                Env:   env,
27✔
512
                Cmd:   cmd,
27✔
513
        }, container.HostConfig{}, network.NetworkingConfig{}, "", stdout, stderr)
27✔
514
}
27✔
515

516
func DockerRunOnceWithConfig(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string, stdout, stderr io.Writer) error {
50✔
517
        // Cannot rely on docker's auto remove because
50✔
518
        //   1. We must inspect exit code after container stops
50✔
519
        //   2. Context cancellation may happen after start
50✔
520
        container, err := DockerStart(ctx, config, hostConfig, networkingConfig, containerName)
50✔
521
        if err != nil {
56✔
522
                return err
6✔
523
        }
6✔
524
        defer DockerRemove(container)
44✔
525
        return DockerStreamLogs(ctx, container, stdout, stderr)
44✔
526
}
527

528
var ErrContainerKilled = errors.New("exit 137")
529

530
func DockerStreamLogs(ctx context.Context, containerId string, stdout, stderr io.Writer, opts ...func(*container.LogsOptions)) error {
53✔
531
        logsOptions := container.LogsOptions{
53✔
532
                ShowStdout: true,
53✔
533
                ShowStderr: true,
53✔
534
                Follow:     true,
53✔
535
        }
53✔
536
        for _, apply := range opts {
62✔
537
                apply(&logsOptions)
9✔
538
        }
9✔
539
        // Stream logs
540
        logs, err := Docker.ContainerLogs(ctx, containerId, logsOptions)
53✔
541
        if err != nil {
57✔
542
                return errors.Errorf("failed to read docker logs: %w", err)
4✔
543
        }
4✔
544
        defer logs.Close()
49✔
545
        if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil {
50✔
546
                return errors.Errorf("failed to copy docker logs: %w", err)
1✔
547
        }
1✔
548
        // Check exit code
549
        resp, err := Docker.ContainerInspect(ctx, containerId)
48✔
550
        if err != nil {
50✔
551
                return errors.Errorf("failed to inspect docker container: %w", err)
2✔
552
        }
2✔
553
        switch resp.State.ExitCode {
46✔
554
        case 0:
39✔
555
                return nil
39✔
556
        case 137:
1✔
557
                err = ErrContainerKilled
1✔
558
        default:
6✔
559
                err = errors.Errorf("exit %d", resp.State.ExitCode)
6✔
560
        }
561
        return errors.Errorf("error running container: %w", err)
7✔
562
}
563

564
func DockerStreamLogsOnce(ctx context.Context, containerId string, stdout, stderr io.Writer) error {
3✔
565
        logs, err := Docker.ContainerLogs(ctx, containerId, container.LogsOptions{
3✔
566
                ShowStdout: true,
3✔
567
                ShowStderr: true,
3✔
568
        })
3✔
569
        if err != nil {
6✔
570
                return errors.Errorf("failed to read docker logs: %w", err)
3✔
571
        }
3✔
572
        defer logs.Close()
×
573
        if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil {
×
574
                return errors.Errorf("failed to copy docker logs: %w", err)
×
575
        }
×
576
        return nil
×
577
}
578

579
// Exec a command once inside a container, returning stdout and throwing error on non-zero exit code.
580
func DockerExecOnce(ctx context.Context, containerId string, env []string, cmd []string) (string, error) {
2✔
581
        stderr := io.Discard
2✔
582
        if viper.GetBool("DEBUG") {
4✔
583
                stderr = os.Stderr
2✔
584
        }
2✔
585
        var out bytes.Buffer
2✔
586
        err := DockerExecOnceWithStream(ctx, containerId, "", env, cmd, &out, stderr)
2✔
587
        return out.String(), err
2✔
588
}
589

590
func DockerExecOnceWithStream(ctx context.Context, containerId, workdir string, env, cmd []string, stdout, stderr io.Writer) error {
2✔
591
        // Reset shadow database
2✔
592
        exec, err := Docker.ContainerExecCreate(ctx, containerId, container.ExecOptions{
2✔
593
                Env:          env,
2✔
594
                Cmd:          cmd,
2✔
595
                WorkingDir:   workdir,
2✔
596
                AttachStderr: true,
2✔
597
                AttachStdout: true,
2✔
598
        })
2✔
599
        if err != nil {
3✔
600
                return errors.Errorf("failed to exec docker create: %w", err)
1✔
601
        }
1✔
602
        // Read exec output
603
        resp, err := Docker.ContainerExecAttach(ctx, exec.ID, container.ExecStartOptions{})
1✔
604
        if err != nil {
2✔
605
                return errors.Errorf("failed to exec docker attach: %w", err)
1✔
606
        }
1✔
607
        defer resp.Close()
×
608
        // Capture error details
×
609
        if _, err := stdcopy.StdCopy(stdout, stderr, resp.Reader); err != nil {
×
610
                return errors.Errorf("failed to copy docker logs: %w", err)
×
611
        }
×
612
        // Get the exit code
613
        iresp, err := Docker.ContainerExecInspect(ctx, exec.ID)
×
614
        if err != nil {
×
615
                return errors.Errorf("failed to exec docker inspect: %w", err)
×
616
        }
×
617
        if iresp.ExitCode > 0 {
×
618
                err = errors.New("error executing command")
×
619
        }
×
620
        return err
×
621
}
622

623
func IsDockerRunning(ctx context.Context) bool {
5✔
624
        _, err := Docker.Ping(ctx)
5✔
625
        return !client.IsErrConnectionFailed(err)
5✔
626
}
5✔
627

628
var portErrorPattern = regexp.MustCompile("Bind for (.*) failed: port is already allocated")
629

630
func parsePortBindError(err error) string {
1✔
631
        matches := portErrorPattern.FindStringSubmatch(err.Error())
1✔
632
        if len(matches) > 1 {
1✔
633
                return matches[len(matches)-1]
×
634
        }
×
635
        return ""
1✔
636
}
637

638
func suggestDockerStop(ctx context.Context, hostPort string) string {
×
639
        if containers, err := Docker.ContainerList(ctx, container.ListOptions{}); err == nil {
×
640
                for _, c := range containers {
×
641
                        for _, p := range c.Ports {
×
642
                                if fmt.Sprintf("%s:%d", p.IP, p.PublicPort) == hostPort {
×
643
                                        if project, ok := c.Labels[CliProjectLabel]; ok {
×
644
                                                return "\nTry stopping the running project with " + Aqua("supabase stop --project-id "+project)
×
645
                                        } else {
×
646
                                                name := c.ID
×
647
                                                if len(c.Names) > 0 {
×
648
                                                        name = c.Names[0]
×
649
                                                }
×
650
                                                return "\nTry stopping the running container with " + Aqua("docker stop "+name)
×
651
                                        }
652
                                }
653
                        }
654
                }
655
        }
656
        return ""
×
657
}
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