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

supabase / cli / 15583807449

11 Jun 2025 11:36AM UTC coverage: 55.106% (+0.02%) from 55.086%
15583807449

Pull #3680

github

web-flow
Merge ecceb62b8 into 06aaa3fcf
Pull Request #3680: feat: add restart command

42 of 65 new or added lines in 4 files covered. (64.62%)

10 existing lines in 3 files now uncovered.

6017 of 10919 relevant lines covered (55.11%)

6.13 hits per line

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

76.61
/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

39
var Docker = NewDocker()
40

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

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

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

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

93
// NoBackupVolume TODO: encapsulate this state in a class
94
var NoBackupVolume = false
95

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

150
func DockerRestartAll(ctx context.Context, w io.Writer, projectId string) error {
4✔
151
        fmt.Fprintln(w, "Restarting containers...")
4✔
152
        args := CliProjectFilter(projectId)
4✔
153
        containers, err := Docker.ContainerList(ctx, container.ListOptions{
4✔
154
                All:     true,
4✔
155
                Filters: args,
4✔
156
        })
4✔
157
        if err != nil {
5✔
158
                return errors.Errorf("failed to list containers: %w", err)
1✔
159
        }
1✔
160
        // Restart containers
161
        var ids []string
3✔
162
        for _, c := range containers {
5✔
163
                if c.State == "running" {
3✔
164
                        ids = append(ids, c.ID)
1✔
165
                }
1✔
166
        }
167
        result := WaitAll(ids, func(id string) error {
4✔
168
                if err := Docker.ContainerRestart(ctx, id, container.StopOptions{}); err != nil {
1✔
NEW
169
                        return errors.Errorf("failed to stop container: %w", err)
×
NEW
170
                }
×
171
                return nil
1✔
172
        })
173
        if err := errors.Join(result...); err != nil {
3✔
NEW
174
                return err
×
NEW
175
        }
×
176

177
        return nil
3✔
178
}
179

180
func CliProjectFilter(projectId string) filters.Args {
19✔
181
        if len(projectId) == 0 {
22✔
182
                return filters.NewArgs(
3✔
183
                        filters.Arg("label", CliProjectLabel),
3✔
184
                )
3✔
185
        }
3✔
186
        return filters.NewArgs(
16✔
187
                filters.Arg("label", CliProjectLabel+"="+projectId),
16✔
188
        )
16✔
189
}
190

191
var (
192
        // Only supports one registry per command invocation
193
        registryAuth string
194
        registryOnce sync.Once
195
)
196

197
func GetRegistryAuth() string {
4✔
198
        registryOnce.Do(func() {
5✔
199
                config := dockerConfig.LoadDefaultConfigFile(os.Stderr)
1✔
200
                // Ref: https://docs.docker.com/engine/api/sdk/examples/#pull-an-image-with-authentication
1✔
201
                auth, err := config.GetAuthConfig(GetRegistry())
1✔
202
                if err != nil {
1✔
203
                        fmt.Fprintln(os.Stderr, "Failed to load registry credentials:", err)
×
204
                        return
×
205
                }
×
206
                encoded, err := json.Marshal(auth)
1✔
207
                if err != nil {
1✔
208
                        fmt.Fprintln(os.Stderr, "Failed to serialise auth config:", err)
×
209
                        return
×
210
                }
×
211
                registryAuth = base64.URLEncoding.EncodeToString(encoded)
1✔
212
        })
213
        return registryAuth
4✔
214
}
215

216
// Defaults to Supabase public ECR for faster image pull
217
const defaultRegistry = "public.ecr.aws"
218

219
func GetRegistry() string {
246✔
220
        registry := viper.GetString("INTERNAL_IMAGE_REGISTRY")
246✔
221
        if len(registry) == 0 {
473✔
222
                return defaultRegistry
227✔
223
        }
227✔
224
        return strings.ToLower(registry)
19✔
225
}
226

227
func GetRegistryImageUrl(imageName string) string {
245✔
228
        registry := GetRegistry()
245✔
229
        if registry == "docker.io" {
263✔
230
                return imageName
18✔
231
        }
18✔
232
        // Configure mirror registry
233
        parts := strings.Split(imageName, "/")
227✔
234
        imageName = parts[len(parts)-1]
227✔
235
        return registry + "/supabase/" + imageName
227✔
236
}
237

238
func DockerImagePull(ctx context.Context, imageTag string, w io.Writer) error {
4✔
239
        out, err := Docker.ImagePull(ctx, imageTag, image.PullOptions{
4✔
240
                RegistryAuth: GetRegistryAuth(),
4✔
241
        })
4✔
242
        if err != nil {
5✔
243
                return errors.Errorf("failed to pull docker image: %w", err)
1✔
244
        }
1✔
245
        defer out.Close()
3✔
246
        if err := jsonmessage.DisplayJSONMessagesToStream(out, streams.NewOut(w), nil); err != nil {
5✔
247
                return errors.Errorf("failed to display json stream: %w", err)
2✔
248
        }
2✔
249
        return nil
1✔
250
}
251

252
// Used by unit tests
253
var timeUnit = time.Second
254

255
func DockerImagePullWithRetry(ctx context.Context, image string, retries int) error {
2✔
256
        err := DockerImagePull(ctx, image, os.Stderr)
2✔
257
        for i := 0; i < retries; i++ {
5✔
258
                if err == nil || errors.Is(ctx.Err(), context.Canceled) {
4✔
259
                        break
1✔
260
                }
261
                fmt.Fprintln(os.Stderr, err)
2✔
262
                period := time.Duration(2<<(i+1)) * timeUnit
2✔
263
                fmt.Fprintf(os.Stderr, "Retrying after %v: %s\n", period, image)
2✔
264
                time.Sleep(period)
2✔
265
                err = DockerImagePull(ctx, image, os.Stderr)
2✔
266
        }
267
        return err
2✔
268
}
269

270
func DockerPullImageIfNotCached(ctx context.Context, imageName string) error {
87✔
271
        imageUrl := GetRegistryImageUrl(imageName)
87✔
272
        if _, err := Docker.ImageInspect(ctx, imageUrl); err == nil {
160✔
273
                return nil
73✔
274
        } else if !errdefs.IsNotFound(err) {
99✔
275
                return errors.Errorf("failed to inspect docker image: %w", err)
12✔
276
        }
12✔
277
        return DockerImagePullWithRetry(ctx, imageUrl, 2)
2✔
278
}
279

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

282
func DockerStart(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string) (string, error) {
83✔
283
        // Pull container image
83✔
284
        if err := DockerPullImageIfNotCached(ctx, config.Image); err != nil {
94✔
285
                if client.IsErrConnectionFailed(err) {
20✔
286
                        CmdSuggestion = suggestDockerInstall
9✔
287
                }
9✔
288
                return "", err
11✔
289
        }
290
        // Setup default config
291
        config.Image = GetRegistryImageUrl(config.Image)
72✔
292
        if config.Labels == nil {
144✔
293
                config.Labels = make(map[string]string, 2)
72✔
294
        }
72✔
295
        config.Labels[CliProjectLabel] = Config.ProjectId
72✔
296
        config.Labels[composeProjectLabel] = Config.ProjectId
72✔
297
        // Configure container network
72✔
298
        hostConfig.ExtraHosts = append(hostConfig.ExtraHosts, extraHosts...)
72✔
299
        if networkId := viper.GetString("network-id"); len(networkId) > 0 {
72✔
300
                hostConfig.NetworkMode = container.NetworkMode(networkId)
×
301
        } else if len(hostConfig.NetworkMode) == 0 {
132✔
302
                hostConfig.NetworkMode = container.NetworkMode(NetId)
60✔
303
        }
60✔
304
        if err := DockerNetworkCreateIfNotExists(ctx, hostConfig.NetworkMode, config.Labels); err != nil {
72✔
305
                return "", err
×
306
        }
×
307
        // Configure container volumes
308
        var binds, sources []string
72✔
309
        for _, bind := range hostConfig.Binds {
109✔
310
                spec, err := loader.ParseVolume(bind)
37✔
311
                if err != nil {
37✔
312
                        return "", errors.Errorf("failed to parse docker volume: %w", err)
×
313
                }
×
314
                if spec.Type != string(mount.TypeVolume) {
54✔
315
                        binds = append(binds, bind)
17✔
316
                } else if len(spec.Source) > 0 {
57✔
317
                        sources = append(sources, spec.Source)
20✔
318
                }
20✔
319
        }
320
        // Skip named volume for BitBucket pipeline
321
        if os.Getenv("BITBUCKET_CLONE_DIR") != "" {
72✔
322
                hostConfig.Binds = binds
×
323
                // Bitbucket doesn't allow for --security-opt option to be set
×
324
                // https://support.atlassian.com/bitbucket-cloud/docs/run-docker-commands-in-bitbucket-pipelines/#Full-list-of-restricted-commands
×
325
                hostConfig.SecurityOpt = nil
×
326
        } else {
72✔
327
                // Create named volumes with labels
72✔
328
                for _, name := range sources {
92✔
329
                        if _, err := Docker.VolumeCreate(ctx, volume.CreateOptions{
20✔
330
                                Name:   name,
20✔
331
                                Labels: config.Labels,
20✔
332
                        }); err != nil {
20✔
333
                                return "", errors.Errorf("failed to create volume: %w", err)
×
334
                        }
×
335
                }
336
        }
337
        // Create container from image
338
        resp, err := Docker.ContainerCreate(ctx, &config, &hostConfig, &networkingConfig, nil, containerName)
72✔
339
        if err != nil {
73✔
340
                return "", errors.Errorf("failed to create docker container: %w", err)
1✔
341
        }
1✔
342
        // Run container in background
343
        err = Docker.ContainerStart(ctx, resp.ID, container.StartOptions{})
71✔
344
        if err != nil {
72✔
345
                if hostPort := parsePortBindError(err); len(hostPort) > 0 {
1✔
346
                        CmdSuggestion = suggestDockerStop(ctx, hostPort)
×
347
                        prefix := "Or configure"
×
348
                        if len(CmdSuggestion) == 0 {
×
349
                                prefix = "Try configuring"
×
350
                        }
×
351
                        name := containerName
×
352
                        if endpoint, ok := networkingConfig.EndpointsConfig[NetId]; ok && len(endpoint.Aliases) > 0 {
×
353
                                name = endpoint.Aliases[0]
×
354
                        }
×
355
                        CmdSuggestion += fmt.Sprintf("\n%s a different %s port in %s", prefix, name, Bold(ConfigPath))
×
356
                }
357
                err = errors.Errorf("failed to start docker container: %w", err)
1✔
358
        }
359
        return resp.ID, err
71✔
360
}
361

362
func DockerRemove(containerId string) {
52✔
363
        if err := Docker.ContainerRemove(context.Background(), containerId, container.RemoveOptions{
52✔
364
                RemoveVolumes: true,
52✔
365
                Force:         true,
52✔
366
        }); err != nil {
52✔
367
                fmt.Fprintln(os.Stderr, "Failed to remove container:", containerId, err)
×
368
        }
×
369
}
370

371
type DockerJob struct {
372
        Image string
373
        Env   []string
374
        Cmd   []string
375
}
376

377
func DockerRunJob(ctx context.Context, job DockerJob, stdout, stderr io.Writer) error {
20✔
378
        return DockerRunOnceWithStream(ctx, job.Image, job.Env, job.Cmd, stdout, stderr)
20✔
379
}
20✔
380

381
// Runs a container image exactly once, returning stdout and throwing error on non-zero exit code.
382
func DockerRunOnce(ctx context.Context, image string, env []string, cmd []string) (string, error) {
7✔
383
        stderr := GetDebugLogger()
7✔
384
        var out bytes.Buffer
7✔
385
        err := DockerRunOnceWithStream(ctx, image, env, cmd, &out, stderr)
7✔
386
        return out.String(), err
7✔
387
}
7✔
388

389
func DockerRunOnceWithStream(ctx context.Context, image string, env, cmd []string, stdout, stderr io.Writer) error {
27✔
390
        return DockerRunOnceWithConfig(ctx, container.Config{
27✔
391
                Image: image,
27✔
392
                Env:   env,
27✔
393
                Cmd:   cmd,
27✔
394
        }, container.HostConfig{}, network.NetworkingConfig{}, "", stdout, stderr)
27✔
395
}
27✔
396

397
func DockerRunOnceWithConfig(ctx context.Context, config container.Config, hostConfig container.HostConfig, networkingConfig network.NetworkingConfig, containerName string, stdout, stderr io.Writer) error {
50✔
398
        // Cannot rely on docker's auto remove because
50✔
399
        //   1. We must inspect exit code after container stops
50✔
400
        //   2. Context cancellation may happen after start
50✔
401
        container, err := DockerStart(ctx, config, hostConfig, networkingConfig, containerName)
50✔
402
        if err != nil {
56✔
403
                return err
6✔
404
        }
6✔
405
        defer DockerRemove(container)
44✔
406
        return DockerStreamLogs(ctx, container, stdout, stderr)
44✔
407
}
408

409
func DockerStreamLogs(ctx context.Context, containerId string, stdout, stderr io.Writer) error {
45✔
410
        // Stream logs
45✔
411
        logs, err := Docker.ContainerLogs(ctx, containerId, container.LogsOptions{
45✔
412
                ShowStdout: true,
45✔
413
                ShowStderr: true,
45✔
414
                Follow:     true,
45✔
415
        })
45✔
416
        if err != nil {
47✔
417
                return errors.Errorf("failed to read docker logs: %w", err)
2✔
418
        }
2✔
419
        defer logs.Close()
43✔
420
        if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil {
44✔
421
                return errors.Errorf("failed to copy docker logs: %w", err)
1✔
422
        }
1✔
423
        // Check exit code
424
        resp, err := Docker.ContainerInspect(ctx, containerId)
42✔
425
        if err != nil {
43✔
426
                return errors.Errorf("failed to inspect docker container: %w", err)
1✔
427
        }
1✔
428
        if resp.State.ExitCode > 0 {
43✔
429
                return errors.Errorf("error running container: exit %d", resp.State.ExitCode)
2✔
430
        }
2✔
431
        return nil
39✔
432
}
433

434
func DockerStreamLogsOnce(ctx context.Context, containerId string, stdout, stderr io.Writer) error {
3✔
435
        logs, err := Docker.ContainerLogs(ctx, containerId, container.LogsOptions{
3✔
436
                ShowStdout: true,
3✔
437
                ShowStderr: true,
3✔
438
        })
3✔
439
        if err != nil {
6✔
440
                return errors.Errorf("failed to read docker logs: %w", err)
3✔
441
        }
3✔
442
        defer logs.Close()
×
443
        if _, err := stdcopy.StdCopy(stdout, stderr, logs); err != nil {
×
444
                return errors.Errorf("failed to copy docker logs: %w", err)
×
445
        }
×
446
        return nil
×
447
}
448

449
// Exec a command once inside a container, returning stdout and throwing error on non-zero exit code.
450
func DockerExecOnce(ctx context.Context, containerId string, env []string, cmd []string) (string, error) {
2✔
451
        stderr := io.Discard
2✔
452
        if viper.GetBool("DEBUG") {
4✔
453
                stderr = os.Stderr
2✔
454
        }
2✔
455
        var out bytes.Buffer
2✔
456
        err := DockerExecOnceWithStream(ctx, containerId, "", env, cmd, &out, stderr)
2✔
457
        return out.String(), err
2✔
458
}
459

460
func DockerExecOnceWithStream(ctx context.Context, containerId, workdir string, env, cmd []string, stdout, stderr io.Writer) error {
2✔
461
        // Reset shadow database
2✔
462
        exec, err := Docker.ContainerExecCreate(ctx, containerId, container.ExecOptions{
2✔
463
                Env:          env,
2✔
464
                Cmd:          cmd,
2✔
465
                WorkingDir:   workdir,
2✔
466
                AttachStderr: true,
2✔
467
                AttachStdout: true,
2✔
468
        })
2✔
469
        if err != nil {
3✔
470
                return errors.Errorf("failed to exec docker create: %w", err)
1✔
471
        }
1✔
472
        // Read exec output
473
        resp, err := Docker.ContainerExecAttach(ctx, exec.ID, container.ExecStartOptions{})
1✔
474
        if err != nil {
2✔
475
                return errors.Errorf("failed to exec docker attach: %w", err)
1✔
476
        }
1✔
477
        defer resp.Close()
×
478
        // Capture error details
×
479
        if _, err := stdcopy.StdCopy(stdout, stderr, resp.Reader); err != nil {
×
480
                return errors.Errorf("failed to copy docker logs: %w", err)
×
481
        }
×
482
        // Get the exit code
483
        iresp, err := Docker.ContainerExecInspect(ctx, exec.ID)
×
484
        if err != nil {
×
485
                return errors.Errorf("failed to exec docker inspect: %w", err)
×
486
        }
×
487
        if iresp.ExitCode > 0 {
×
488
                err = errors.New("error executing command")
×
489
        }
×
490
        return err
×
491
}
492

493
func IsDockerRunning(ctx context.Context) bool {
5✔
494
        _, err := Docker.Ping(ctx)
5✔
495
        return !client.IsErrConnectionFailed(err)
5✔
496
}
5✔
497

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

500
func parsePortBindError(err error) string {
1✔
501
        matches := portErrorPattern.FindStringSubmatch(err.Error())
1✔
502
        if len(matches) > 1 {
1✔
503
                return matches[len(matches)-1]
×
504
        }
×
505
        return ""
1✔
506
}
507

508
func suggestDockerStop(ctx context.Context, hostPort string) string {
×
509
        if containers, err := Docker.ContainerList(ctx, container.ListOptions{}); err == nil {
×
510
                for _, c := range containers {
×
511
                        for _, p := range c.Ports {
×
512
                                if fmt.Sprintf("%s:%d", p.IP, p.PublicPort) == hostPort {
×
513
                                        if project, ok := c.Labels[CliProjectLabel]; ok {
×
514
                                                return "\nTry stopping the running project with " + Aqua("supabase stop --project-id "+project)
×
515
                                        } else {
×
516
                                                name := c.ID
×
517
                                                if len(c.Names) > 0 {
×
518
                                                        name = c.Names[0]
×
519
                                                }
×
520
                                                return "\nTry stopping the running container with " + Aqua("docker stop "+name)
×
521
                                        }
522
                                }
523
                        }
524
                }
525
        }
526
        return ""
×
527
}
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