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

tensorchord / envd / 18149755418

01 Oct 2025 02:45AM UTC coverage: 42.475% (-0.3%) from 42.787%
18149755418

push

github

web-flow
chore(deps): bump github.com/go-viper/mapstructure/v2 from 2.3.0 to 2.4.0 (#2034)

chore(deps): bump github.com/go-viper/mapstructure/v2

Bumps [github.com/go-viper/mapstructure/v2](https://github.com/go-viper/mapstructure) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/go-viper/mapstructure/releases)
- [Changelog](https://github.com/go-viper/mapstructure/blob/main/CHANGELOG.md)
- [Commits](https://github.com/go-viper/mapstructure/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: github.com/go-viper/mapstructure/v2
  dependency-version: 2.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

5182 of 12200 relevant lines covered (42.48%)

165.16 hits per line

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

43.51
/pkg/envd/docker.go
1
// Copyright 2023 The envd Authors
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//      http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
package envd
16

17
import (
18
        "context"
19
        "encoding/json"
20
        "fmt"
21
        "path/filepath"
22
        "runtime"
23
        "strconv"
24
        "strings"
25
        "time"
26

27
        "github.com/cockroachdb/errors"
28
        "github.com/containerd/errdefs"
29
        "github.com/docker/cli/opts"
30
        "github.com/docker/docker/api/types/container"
31
        "github.com/docker/docker/api/types/filters"
32
        dockerimage "github.com/docker/docker/api/types/image"
33
        "github.com/docker/docker/api/types/mount"
34
        "github.com/docker/docker/client"
35
        "github.com/docker/go-connections/nat"
36
        dockerutils "github.com/docker/go-units"
37
        "github.com/sirupsen/logrus"
38

39
        envdconfig "github.com/tensorchord/envd/pkg/config"
40
        "github.com/tensorchord/envd/pkg/lang/ir"
41
        "github.com/tensorchord/envd/pkg/lang/version"
42
        "github.com/tensorchord/envd/pkg/ssh"
43
        sshconfig "github.com/tensorchord/envd/pkg/ssh/config"
44
        "github.com/tensorchord/envd/pkg/types"
45
        "github.com/tensorchord/envd/pkg/util/fileutil"
46
        "github.com/tensorchord/envd/pkg/util/netutil"
47
)
48

49
type dockerEngine struct {
50
        *client.Client
51
}
52

53
func dockerFilters(gpu bool) filters.Args {
1✔
54
        f := filters.NewArgs()
1✔
55
        f.Add("label", fmt.Sprintf("%s=%s", types.ImageLabelVendor, types.ImageVendorEnvd))
1✔
56
        if gpu {
1✔
57
                f.Add("label", fmt.Sprintf("%s=true", types.ImageLabelGPU))
×
58
        }
×
59
        return f
1✔
60
}
61

62
func dockerFiltersWithName(name string) filters.Args {
26✔
63
        f := filters.NewArgs()
26✔
64
        f.Add("reference", name)
26✔
65
        return f
26✔
66
}
26✔
67

68
func (e dockerEngine) ListImage(ctx context.Context) ([]types.EnvdImage, error) {
×
69
        images, err := e.ImageList(ctx, dockerimage.ListOptions{
×
70
                Filters: dockerFilters(false),
×
71
        })
×
72
        if err != nil {
×
73
                return nil, errors.Wrap(err, "failed to get the images")
×
74
        }
×
75

76
        envdImgs := make([]types.EnvdImage, 0)
×
77
        for _, img := range images {
×
78
                envdImg, err := types.NewImageFromSummary(img)
×
79
                if err != nil {
×
80
                        return nil, errors.Wrapf(err, "failed to create envd image `%s` from the docker image", img.ID)
×
81
                }
×
82
                envdImgs = append(envdImgs, *envdImg)
×
83
        }
84
        return envdImgs, nil
×
85
}
86

87
func (e dockerEngine) GetEnvironment(ctx context.Context, env string) (*types.EnvdEnvironment, error) {
×
88
        ctrs, err := e.ContainerList(ctx, container.ListOptions{
×
89
                Filters: dockerFiltersWithName(env),
×
90
        })
×
91
        if err != nil {
×
92
                return nil, errors.Wrapf(err, "failed to get container: %s", env)
×
93
        }
×
94

95
        if len(ctrs) <= 0 {
×
96
                return nil, errors.Newf("can not find the container: %s", env)
×
97
        }
×
98

99
        environment, err := types.NewEnvironmentFromContainer(ctrs[0])
×
100
        if err != nil {
×
101
                return nil, errors.Wrap(err, "failed to create env from the container")
×
102
        }
×
103
        return environment, nil
×
104
}
105

106
func (e dockerEngine) ListEnvironment(
107
        ctx context.Context) ([]types.EnvdEnvironment, error) {
1✔
108
        ctrs, err := e.ContainerList(ctx, container.ListOptions{
1✔
109
                Filters: dockerFilters(false),
1✔
110
        })
1✔
111
        if err != nil {
1✔
112
                return nil, errors.Wrap(err, "failed to list containers")
×
113
        }
×
114

115
        envs := make([]types.EnvdEnvironment, 0)
1✔
116
        for _, ctr := range ctrs {
1✔
117
                env, err := types.NewEnvironmentFromContainer(ctr)
×
118
                if err != nil {
×
119
                        return nil, errors.Wrap(err, "failed to create env from the container")
×
120
                }
×
121
                envs = append(envs, *env)
×
122
        }
123
        return envs, nil
1✔
124
}
125

126
func (e dockerEngine) PauseEnvironment(ctx context.Context, env string) (string, error) {
×
127
        logger := logrus.WithFields(logrus.Fields{
×
128
                "env": env,
×
129
        })
×
130
        logger.Debug("pausing environment")
×
131
        err := e.ContainerPause(ctx, env)
×
132
        if err != nil {
×
133
                errCause := errors.UnwrapAll(err).Error()
×
134
                switch {
×
135
                case strings.Contains(errCause, "is already paused"):
×
136
                        logger.Debug("container is already paused, there is no need to pause it again")
×
137
                        return "", nil
×
138
                case strings.Contains(errCause, "No such container"):
×
139
                        logger.Debug("container is not found, there is no need to pause it")
×
140
                        return "", errors.New("container not found")
×
141
                default:
×
142
                        return "", errors.Wrap(err, "failed to pause container")
×
143
                }
144
        }
145
        return env, nil
×
146
}
147

148
func (e dockerEngine) ResumeEnvironment(ctx context.Context, env string) (string, error) {
×
149
        logger := logrus.WithFields(logrus.Fields{
×
150
                "env": env,
×
151
        })
×
152
        logger.Debug("resuming environment")
×
153

×
154
        err := e.ContainerUnpause(ctx, env)
×
155
        if err != nil {
×
156
                errCause := errors.UnwrapAll(err).Error()
×
157
                switch {
×
158
                case strings.Contains(errCause, "is not paused"):
×
159
                        logger.Debug("container is not paused, there is no need to resume")
×
160
                        return "", nil
×
161
                case strings.Contains(errCause, "No such container"):
×
162
                        logger.Debug("container is not found, there is no need to resume it")
×
163
                        return "", errors.New("container not found")
×
164
                default:
×
165
                        return "", errors.Wrap(err, "failed to resume container")
×
166
                }
167
        }
168
        return env, nil
×
169
}
170

171
// ListImageDependency gets the dependencies of the given environment.
172
func (e dockerEngine) ListImageDependency(ctx context.Context, image string) (*types.Dependency, error) {
×
173
        logger := logrus.WithFields(logrus.Fields{
×
174
                "image": image,
×
175
        })
×
176
        logger.Debug("getting dependencies")
×
177
        images, err := e.ImageList(ctx, dockerimage.ListOptions{
×
178
                Filters: dockerFiltersWithName(image),
×
179
        })
×
180
        if err != nil {
×
181
                return nil, err
×
182
        }
×
183
        if len(images) == 0 {
×
184
                return nil, errors.Errorf("image %s not found", image)
×
185
        }
×
186

187
        img := images[0]
×
188
        dep, err := types.NewDependencyFromImageSummary(img)
×
189
        if err != nil {
×
190
                return nil, errors.Wrap(err, "failed to create dependency from image")
×
191
        }
×
192
        return dep, nil
×
193
}
194

195
func (e dockerEngine) getVerFromImageLabel(ctx context.Context, env string) (version.Getter, error) {
12✔
196
        ctr, err := e.GetImage(ctx, env)
12✔
197
        if err != nil {
12✔
198
                return nil, errors.Wrapf(err, "failed to inspect image: %s", env)
×
199
        }
×
200
        ver, ok := ctr.Labels[types.ImageLabelSyntaxVer]
12✔
201
        if !ok {
12✔
202
                return version.NewByVersion("v1"), nil
×
203
        }
×
204
        return version.NewByVersion(ver), nil
12✔
205
}
206

207
func (e dockerEngine) listEnvGeneralGraph(ctx context.Context, env string, g ir.Graph) (ir.Graph, error) {
12✔
208
        ctr, err := e.GetImage(ctx, env)
12✔
209
        if err != nil {
12✔
210
                return nil, errors.Wrapf(err, "failed to inspect image: %s", env)
×
211
        }
×
212
        code, ok := ctr.Labels[types.GeneralGraphCode]
12✔
213
        if !ok {
12✔
214
                return nil, errors.Newf("failed to get runtime graph label from image: %s", env)
×
215
        }
×
216
        logrus.WithField("env", env).Debugf("general graph: %s", code)
12✔
217
        newg, err := g.GeneralGraphFromLabel([]byte(code))
12✔
218
        if err != nil {
12✔
219
                return nil, errors.Wrapf(err, "failed to create runtime graph from the image: %s", env)
×
220
        }
×
221
        return newg, err
12✔
222
}
223

224
func (e dockerEngine) ListEnvRuntimeGraph(ctx context.Context, env string) (*ir.RuntimeGraph, error) {
4✔
225
        ctr, err := e.ContainerInspect(ctx, env)
4✔
226
        if err != nil {
4✔
227
                return nil, errors.Wrapf(err, "failed to inspect container: %s", env)
×
228
        }
×
229
        code, ok := ctr.Config.Labels[types.RuntimeGraphCode]
4✔
230
        if !ok {
4✔
231
                return nil, errors.Newf("failed to get runtime graph label from container: %s", env)
×
232
        }
×
233
        logrus.WithField("env", env).Debugf("runtime graph: %s", code)
4✔
234
        rg := ir.RuntimeGraph{}
4✔
235
        err = rg.Load([]byte(code))
4✔
236
        if err != nil {
4✔
237
                return nil, errors.Wrapf(err, "failed to create runtime graph from the container: %s", env)
×
238
        }
×
239
        return &rg, err
4✔
240
}
241

242
// ListEnvDependency gets the dependencies of the given environment.
243
func (e dockerEngine) ListEnvDependency(
244
        ctx context.Context, env string) (*types.Dependency, error) {
1✔
245
        logger := logrus.WithFields(logrus.Fields{
1✔
246
                "env": env,
1✔
247
        })
1✔
248
        logger.Debug("getting dependencies")
1✔
249
        ctr, err := e.ContainerInspect(ctx, env)
1✔
250
        if err != nil {
1✔
251
                return nil, errors.Wrap(err, "failed to get container")
×
252
        }
×
253
        dep, err := types.NewDependencyFromContainerJSON(ctr)
1✔
254
        if err != nil {
1✔
255
                return nil, errors.Wrap(err, "failed to create dependency from the container")
×
256
        }
×
257
        return dep, nil
1✔
258
}
259

260
func (e dockerEngine) ListEnvPortBinding(ctx context.Context, env string) ([]types.PortBinding, error) {
1✔
261
        logrus.WithField("env", env).Debug("getting env port bindings")
1✔
262
        ctr, err := e.ContainerInspect(ctx, env)
1✔
263
        if err != nil {
1✔
264
                return nil, errors.Wrap(err, "failed to get container")
×
265
        }
×
266

267
        ports, err := types.NewPortBindingFromContainerJSON(ctr)
1✔
268
        if err != nil {
1✔
269
                return nil, errors.Wrap(err, "failed to get port binding from container json")
×
270
        }
×
271

272
        return ports, nil
1✔
273
}
274

275
func (e dockerEngine) GetInfo(ctx context.Context) (*types.EnvdInfo, error) {
×
276
        info, err := e.Info(ctx)
×
277
        if err != nil {
×
278
                return nil, errors.Wrap(err, "failed to get docker client info")
×
279
        }
×
280
        return &types.EnvdInfo{
×
281
                Info: info,
×
282
        }, nil
×
283
}
284

285
func (e dockerEngine) CleanEnvdIfExists(ctx context.Context, name string, force bool) error {
12✔
286
        created, err := e.Exists(ctx, name)
12✔
287
        if err != nil {
12✔
288
                return err
×
289
        }
×
290
        if !created {
24✔
291
                return nil
12✔
292
        }
12✔
293
        return e.ContainerRemove(ctx, name, container.RemoveOptions{Force: force})
×
294
}
295

296
func (e dockerEngine) Exists(ctx context.Context, cname string) (bool, error) {
12✔
297
        _, err := e.ContainerInspect(ctx, cname)
12✔
298
        if err != nil {
24✔
299
                if errdefs.IsNotFound(err) {
24✔
300
                        return false, nil
12✔
301
                }
12✔
302
                return false, err
×
303
        }
304
        return true, nil
×
305
}
306

307
func (e dockerEngine) IsRunning(ctx context.Context, cname string) (bool, error) {
30✔
308
        container, err := e.ContainerInspect(ctx, cname)
30✔
309
        if err != nil {
42✔
310
                if errdefs.IsNotFound(err) {
24✔
311
                        return false, nil
12✔
312
                }
12✔
313
                return false, err
×
314
        }
315
        return container.State.Running, nil
18✔
316
}
317

318
func (e dockerEngine) GenerateSSHConfig(name, iface, privateKeyPath string,
319
        startResult *StartResult) (sshconfig.EntryOptions, error) {
12✔
320
        eo := sshconfig.EntryOptions{
12✔
321
                Name:               name,
12✔
322
                IFace:              iface,
12✔
323
                Port:               startResult.SSHPort,
12✔
324
                PrivateKeyPath:     privateKeyPath,
12✔
325
                EnableHostKeyCheck: false,
12✔
326
                EnableAgentForward: true,
12✔
327
        }
12✔
328
        return eo, nil
12✔
329
}
12✔
330

331
func (e dockerEngine) Attach(name, iface, privateKeyPath string,
332
        startResult *StartResult, g ir.Graph) error {
×
333
        opt := ssh.DefaultOptions()
×
334
        opt.Server = iface
×
335
        opt.PrivateKeyPath = privateKeyPath
×
336
        opt.Port = startResult.SSHPort
×
337
        sshClient, err := ssh.NewClient(opt)
×
338
        if err != nil {
×
339
                return errors.Wrap(err, "failed to create the ssh client")
×
340
        }
×
341
        opt.Server = iface
×
342

×
343
        if err := sshClient.Attach(); err != nil {
×
344
                return errors.Wrap(err, "failed to attach to the container")
×
345
        }
×
346
        return nil
×
347
}
348

349
// StartEnvd creates the container for the given tag and container name.
350
func (e dockerEngine) StartEnvd(ctx context.Context, so StartOptions) (*StartResult, error) {
12✔
351
        logger := logrus.WithFields(logrus.Fields{
12✔
352
                "tag":           so.Image,
12✔
353
                "environment":   so.EnvironmentName,
12✔
354
                "gpu-set":       so.GPUSet,
12✔
355
                "shm":           so.ShmSize,
12✔
356
                "cpu":           so.NumCPU,
12✔
357
                "cpu-set":       so.CPUSet,
12✔
358
                "memory":        so.NumMem,
12✔
359
                "build-context": so.BuildContext,
12✔
360
        })
12✔
361

12✔
362
        bar := InitProgressBar(5)
12✔
363
        defer bar.Finish()
12✔
364
        bar.UpdateTitle("configure the environment")
12✔
365

12✔
366
        if len(so.GPUSet) > 0 {
12✔
367
                nvruntimeExists, err := e.GPUEnabled(ctx)
×
368
                if err != nil {
×
369
                        return nil, errors.Wrap(err, "failed to check if nvidia-runtime is installed")
×
370
                }
×
371
                if !nvruntimeExists {
×
372
                        return nil, errors.New("GPU is required but nvidia container runtime is not installed, please refer to https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html#docker")
×
373
                }
×
374
        }
375

376
        sshPortInHost, err := netutil.GetFreePort()
12✔
377
        if err != nil {
12✔
378
                return nil, errors.Wrap(err, "failed to get a free port")
×
379
        }
×
380

381
        err = e.CleanEnvdIfExists(ctx, so.EnvironmentName, so.Forced)
12✔
382
        if err != nil {
12✔
383
                return nil, errors.Wrap(err, "failed to clean the envd environment")
×
384
        }
×
385
        config := &container.Config{
12✔
386
                Image:        so.Image,
12✔
387
                ExposedPorts: nat.PortSet{},
12✔
388
        }
12✔
389
        base := fileutil.EnvdHomeDir(filepath.Base(so.BuildContext))
12✔
390
        config.WorkingDir = base
12✔
391

12✔
392
        var g ir.Graph
12✔
393
        if so.Image == "" {
12✔
394
                g = so.DockerSource.Graph
×
395
        } else {
12✔
396
                // Use specified image as the running image
12✔
397
                getter, err := e.getVerFromImageLabel(ctx, so.Image)
12✔
398
                if err != nil {
12✔
399
                        return nil, errors.Wrap(err, "failed to get the version from the image label")
×
400
                }
×
401
                defaultGraph := getter.GetDefaultGraph()
12✔
402
                if err != nil {
12✔
403
                        return nil, errors.Wrap(err, "failed to get the graph from the image")
×
404
                }
×
405
                g, err = e.listEnvGeneralGraph(ctx, so.Image, defaultGraph)
12✔
406
                if err != nil {
12✔
407
                        return nil, errors.Wrap(err, "failed to get the graph from the image")
×
408
                }
×
409
                so.DockerSource.Graph = g
12✔
410
        }
411

412
        if so.DockerSource == nil || so.DockerSource.Graph == nil {
12✔
413
                return nil, errors.New("failed to get the docker-specific options")
×
414
        }
×
415

416
        mountOption := make([]mount.Mount, 0,
12✔
417
                len(so.DockerSource.MountOptions)+len(g.GetMount())+1)
12✔
418
        for _, option := range so.DockerSource.MountOptions {
12✔
419
                mStr := strings.Split(option, ":")
×
420
                if len(mStr) != 2 {
×
421
                        return nil, errors.Newf("Invalid mount options %s", option)
×
422
                }
×
423

424
                logger.WithFields(logrus.Fields{
×
425
                        "mount-path":     mStr[0],
×
426
                        "container-path": mStr[1],
×
427
                }).Debug("setting up container working directory")
×
428
                mountOption = append(mountOption, mount.Mount{
×
429
                        Type:   mount.TypeBind,
×
430
                        Source: mStr[0],
×
431
                        Target: mStr[1],
×
432
                })
×
433
        }
434
        for _, m := range g.GetMount() {
12✔
435
                logger.WithFields(logrus.Fields{
×
436
                        "mount-path":     m.Source,
×
437
                        "container-path": m.Destination,
×
438
                }).Debug("setting up declared mount directory")
×
439
                mountOption = append(mountOption, mount.Mount{
×
440
                        Type:   mount.TypeBind,
×
441
                        Source: m.Source,
×
442
                        Target: m.Destination,
×
443
                })
×
444
        }
×
445

446
        mountOption = append(mountOption, mount.Mount{
12✔
447
                Type:   mount.TypeBind,
12✔
448
                Source: so.BuildContext,
12✔
449
                Target: base,
12✔
450
        })
12✔
451

12✔
452
        logger.WithFields(logrus.Fields{
12✔
453
                "mount-path":  so.BuildContext,
12✔
454
                "working-dir": base,
12✔
455
        }).Debug("setting up container working directory")
12✔
456

12✔
457
        rp := container.RestartPolicy{
12✔
458
                Name: "always",
12✔
459
        }
12✔
460
        hostConfig := &container.HostConfig{
12✔
461
                PortBindings:  nat.PortMap{},
12✔
462
                Mounts:        mountOption,
12✔
463
                RestartPolicy: rp,
12✔
464
        }
12✔
465

12✔
466
        // shared memory size
12✔
467
        if so.ShmSize > 0 {
24✔
468
                hostConfig.ShmSize = int64(so.ShmSize) * 1024 * 1024
12✔
469
        }
12✔
470
        // resource
471
        if len(so.NumCPU) > 0 {
12✔
472
                cpu, err := strconv.ParseFloat(so.NumCPU, 64)
×
473
                if err != nil {
×
474
                        logger.Infof("parse `cpu` error: %v, ignore this argument", err)
×
475
                } else if runtime.GOOS == "windows" {
×
476
                        hostConfig.NanoCPUs = int64(cpu * 10e9)
×
477
                } else {
×
478
                        // refer to https://docs.docker.com/config/containers/resource_constraints/#configure-the-default-cfs-scheduler
×
479
                        // CPU quota and CPU period only work for UNIX platform
×
480
                        hostConfig.CPUQuota = int64(cpu * 10e5)
×
481
                }
×
482
        }
483
        if len(so.CPUSet) > 0 {
12✔
484
                hostConfig.CpusetCpus = so.CPUSet
×
485
        }
×
486
        if len(so.NumMem) > 0 {
12✔
487
                mem, err := dockerutils.RAMInBytes(so.NumMem)
×
488
                if err != nil {
×
489
                        logger.WithError(err).Info("parse `memory` error, ignore this argument")
×
490
                } else {
×
491
                        hostConfig.Memory = mem
×
492
                }
×
493
        }
494

495
        // Configure ssh port.
496
        natPort := nat.Port(fmt.Sprintf("%d/tcp", envdconfig.SSHPortInContainer))
12✔
497
        hostConfig.PortBindings[natPort] = []nat.PortBinding{
12✔
498
                {
12✔
499
                        HostIP:   so.SshdHost,
12✔
500
                        HostPort: strconv.Itoa(sshPortInHost),
12✔
501
                },
12✔
502
        }
12✔
503

12✔
504
        var jupyterPortInHost int
12✔
505
        // TODO(gaocegege): Avoid specific logic to set the port.
12✔
506
        // Add a func to builder to generate all the ports from the build process.
12✔
507
        if g.GetJupyterConfig() != nil {
12✔
508
                jc := g.GetJupyterConfig()
×
509
                if jc.Port != 0 {
×
510
                        jupyterPortInHost = int(jc.Port)
×
511
                } else {
×
512
                        var err error
×
513
                        jupyterPortInHost, err = netutil.GetFreePort()
×
514
                        if err != nil {
×
515
                                return nil, errors.Wrap(err, "failed to get a free port")
×
516
                        }
×
517
                }
518
                natPort := nat.Port(fmt.Sprintf("%d/tcp", envdconfig.JupyterPortInContainer))
×
519
                hostConfig.PortBindings[natPort] = []nat.PortBinding{
×
520
                        {
×
521
                                HostIP:   Localhost,
×
522
                                HostPort: strconv.Itoa(jupyterPortInHost),
×
523
                        },
×
524
                }
×
525
                config.ExposedPorts[natPort] = struct{}{}
×
526
        }
527
        var rStudioPortInHost int
12✔
528
        if g.GetRStudioServerConfig() != nil {
12✔
529
                var err error
×
530
                rStudioPortInHost, err = netutil.GetFreePort()
×
531
                if err != nil {
×
532
                        return nil, errors.Wrap(err, "failed to get a free port")
×
533
                }
×
534
                natPort := nat.Port(fmt.Sprintf("%d/tcp", envdconfig.RStudioServerPortInContainer))
×
535
                hostConfig.PortBindings[natPort] = []nat.PortBinding{
×
536
                        {
×
537
                                HostIP:   Localhost,
×
538
                                HostPort: strconv.Itoa(rStudioPortInHost),
×
539
                        },
×
540
                }
×
541
                config.ExposedPorts[natPort] = struct{}{}
×
542
        }
543

544
        if len(g.GetExposedPorts()) > 0 {
12✔
545

×
546
                for _, item := range g.GetExposedPorts() {
×
547
                        var err error
×
548
                        if item.HostPort == 0 {
×
549
                                item.HostPort, err = netutil.GetFreePort()
×
550
                                if err != nil {
×
551
                                        return nil, errors.Wrap(err, "failed to get a free port")
×
552
                                }
×
553
                        }
554
                        natPort := nat.Port(fmt.Sprintf("%d/tcp", item.EnvdPort))
×
555
                        hostConfig.PortBindings[natPort] = []nat.PortBinding{
×
556
                                {
×
557
                                        HostIP:   item.ListeningAddr,
×
558
                                        HostPort: strconv.Itoa(item.HostPort),
×
559
                                },
×
560
                        }
×
561
                        config.ExposedPorts[natPort] = struct{}{}
×
562
                }
563
        }
564

565
        if len(so.GPUSet) > 0 {
12✔
566
                logger.Debug("GPU is enabled.")
×
567
                hostConfig.DeviceRequests, err = deviceRequests(so.GPUSet)
×
568
                if err != nil {
×
569
                        return nil, errors.Wrap(err, "failed to parse gpu-set flag")
×
570

×
571
                }
×
572
        }
573

574
        config.Labels = e.labels(g, so.EnvironmentName,
12✔
575
                sshPortInHost, jupyterPortInHost, rStudioPortInHost)
12✔
576

12✔
577
        logger = logger.WithFields(logrus.Fields{
12✔
578
                "entrypoint":  config.Entrypoint,
12✔
579
                "working-dir": config.WorkingDir,
12✔
580
        })
12✔
581
        logger.Debugf("starting %s container", so.EnvironmentName)
12✔
582

12✔
583
        bar.UpdateTitle("create the environment")
12✔
584
        resp, err := e.ContainerCreate(ctx, config, hostConfig, nil, nil, so.EnvironmentName)
12✔
585
        if err != nil {
12✔
586
                return nil, errors.Wrap(err, "failed to create the container")
×
587
        }
×
588

589
        for _, w := range resp.Warnings {
12✔
590
                logger.Warnf("run with warnings: %s", w)
×
591
        }
×
592

593
        bar.UpdateTitle("start the environment")
12✔
594
        if err := e.ContainerStart(
12✔
595
                ctx, resp.ID, container.StartOptions{}); err != nil {
12✔
596
                errCause := errors.UnwrapAll(err)
×
597
                // Hack to check if the port is already allocated.
×
598
                if strings.Contains(errCause.Error(), "port is already allocated") {
×
599
                        logger.WithError(err).Debug("failed to allocate the port")
×
600
                        return nil, errors.New("port is already allocated in the host")
×
601
                }
×
602
                return nil, errors.Wrap(err, "failed to run the container")
×
603
        }
604

605
        container, err := e.ContainerInspect(ctx, resp.ID)
12✔
606
        if err != nil {
12✔
607
                return nil, errors.Wrap(err, "failed to inspect the container")
×
608
        }
×
609

610
        bar.UpdateTitle("wait for the environment to start")
12✔
611
        if err := e.WaitUntilRunning(
12✔
612
                ctx, container.Name, so.Timeout); err != nil {
12✔
613
                return nil, errors.Wrap(err, "failed to wait until the container is running")
×
614
        }
×
615

616
        bar.UpdateTitle("attach the environment")
12✔
617
        result := &StartResult{
12✔
618
                SSHPort: sshPortInHost,
12✔
619
                Address: container.NetworkSettings.IPAddress,
12✔
620
                Name:    container.Name,
12✔
621
        }
12✔
622
        return result, nil
12✔
623
}
624

625
func (e dockerEngine) Destroy(ctx context.Context, name string) (string, error) {
15✔
626
        logger := logrus.WithField("container", name)
15✔
627

15✔
628
        ctr, err := e.ContainerInspect(ctx, name)
15✔
629
        if err != nil {
19✔
630
                errCause := errors.UnwrapAll(err).Error()
4✔
631
                if strings.Contains(errCause, "No such container") {
8✔
632
                        // If the container is not found, it is already destroyed or the name is wrong.
4✔
633
                        logger.Infof("cannot find container %s, maybe it's already destroyed or the name is wrong", name)
4✔
634
                        return "", nil
4✔
635
                }
4✔
636
                return "", errors.Wrap(err, "failed to inspect the container")
×
637
        }
638

639
        // Refer to https://docs.docker.com/engine/reference/commandline/container_kill/
640
        if err := e.ContainerKill(ctx, name, "KILL"); err != nil {
11✔
641
                errCause := errors.UnwrapAll(err).Error()
×
642
                switch {
×
643
                case strings.Contains(errCause, "is not running"):
×
644
                        // If the container is not running, there is no need to kill it.
×
645
                        logger.Debug("container is not running, there is no need to kill it")
×
646
                case strings.Contains(errCause, "No such container"):
×
647
                        // If the container is not found, it is already destroyed or the name is wrong.
×
648
                        logger.Infof("cannot find container %s, maybe it's already destroyed or the name is wrong", name)
×
649
                        return "", nil
×
650
                default:
×
651
                        return "", errors.Wrap(err, "failed to kill the container")
×
652
                }
653
        }
654

655
        if err := e.ContainerRemove(ctx, name, container.RemoveOptions{}); err != nil {
11✔
656
                return "", errors.Wrap(err, "failed to remove the container")
×
657
        }
×
658

659
        if _, err := e.ImageRemove(ctx, ctr.Image, dockerimage.RemoveOptions{}); err != nil {
11✔
660
                return "", errors.Errorf("remove image %s failed: %w", ctr.Image, err)
×
661
        }
×
662
        logger.Infof("image(%s) is destroyed", ctr.Image)
11✔
663

11✔
664
        return name, nil
11✔
665
}
666

667
func (e dockerEngine) WaitUntilRunning(ctx context.Context,
668
        name string, timeout time.Duration) error {
12✔
669
        logger := logrus.WithField("container", name)
12✔
670
        logger.Debug("waiting to start")
12✔
671

12✔
672
        // First, wait for the container to be marked as started.
12✔
673
        ctxTimeout, cancel := context.WithTimeout(ctx, timeout)
12✔
674
        defer cancel()
12✔
675
        for {
24✔
676
                select {
12✔
677
                case <-time.After(waitingInterval):
12✔
678
                        isRunning, err := e.IsRunning(ctxTimeout, name)
12✔
679
                        if err != nil {
12✔
680
                                // Has not yet started. Keep waiting.
×
681
                                return errors.Wrap(err, "failed to check if container is running")
×
682
                        }
×
683
                        if isRunning {
24✔
684
                                logger.Debug("the container is running")
12✔
685
                                return nil
12✔
686
                        }
12✔
687

688
                case <-ctxTimeout.Done():
×
689
                        container, err := e.ContainerInspect(ctx, name)
×
690
                        if err != nil {
×
691
                                logger.Debugf("failed to inspect container %s", name)
×
692
                        }
×
693
                        state, err := json.Marshal(container.State)
×
694
                        if err != nil {
×
695
                                logger.Debug("failed to marshal container state")
×
696
                        }
×
697
                        logger.Debugf("container state: %s", state)
×
698
                        return errors.Errorf("timeout %s: container did not start", timeout)
×
699
                }
700
        }
701
}
702

703
func (e dockerEngine) GPUEnabled(ctx context.Context) (bool, error) {
×
704
        info, err := e.GetInfo(ctx)
×
705
        if err != nil {
×
706
                return false, errors.Wrap(err, "failed to get docker info")
×
707
        }
×
708
        logrus.WithField("info", info).Debug("docker info")
×
709
        nv := info.Runtimes["nvidia"]
×
710
        if nv.Path != "" {
×
711
                return true, nil
×
712
        } else if strings.HasSuffix(info.KernelVersion, "WSL2") {
×
713
                logrus.Warn("We couldn't detect if your runtime support GPU on WSL2, we will continue to run your environment.")
×
714
                return true, nil
×
715
        }
×
716
        return false, nil
×
717
}
718

719
func (e dockerEngine) GetImage(ctx context.Context, imageName string) (types.EnvdImage, error) {
26✔
720
        images, err := e.ImageList(ctx, dockerimage.ListOptions{
26✔
721
                Filters: dockerFiltersWithName(imageName),
26✔
722
        })
26✔
723
        if err != nil {
26✔
724
                return types.EnvdImage{}, err
×
725
        }
×
726
        if len(images) == 0 {
26✔
727
                return types.EnvdImage{},
×
728
                        errors.Errorf("image %s not found", imageName)
×
729
        }
×
730
        img, err := types.NewImageFromSummary(images[0])
26✔
731
        if err != nil {
26✔
732
                return types.EnvdImage{}, err
×
733
        }
×
734
        return *img, nil
26✔
735
}
736

737
func (e dockerEngine) PruneImage(ctx context.Context) (dockerimage.PruneReport, error) {
×
738
        pruneReport, err := e.ImagesPrune(ctx, filters.Args{})
×
739
        if err != nil {
×
740
                return dockerimage.PruneReport{}, errors.Wrap(err, "failed to prune images")
×
741
        }
×
742
        return pruneReport, nil
×
743
}
744

745
func deviceRequests(value string) ([]container.DeviceRequest, error) {
7✔
746
        if value == "" {
7✔
747
                return nil, nil
×
748
        }
×
749
        if !strings.Contains(value, "capabilities=") {
13✔
750
                value += ",\"capabilities=nvidia,compute,compat32,graphics,utility,video,display\""
6✔
751
        }
6✔
752
        if !strings.Contains(value, "driver=") {
13✔
753
                value += ",driver=nvidia"
6✔
754
        }
6✔
755
        opt := opts.GpuOpts{}
7✔
756
        if err := opt.Set(value); err != nil {
7✔
757
                return nil, err
×
758
        }
×
759
        return opt.Value(), nil
7✔
760
}
761

762
func (e dockerEngine) labels(g ir.Graph, name string,
763
        sshPortInHost, jupyterPortInHost, rstudioServerPortInHost int) map[string]string {
12✔
764
        res := make(map[string]string)
12✔
765
        res[types.ContainerLabelName] = name
12✔
766
        res[types.ContainerLabelSSHPort] = strconv.Itoa(sshPortInHost)
12✔
767
        if g.GetJupyterConfig() != nil {
12✔
768
                res[types.ContainerLabelJupyterAddr] =
×
769
                        fmt.Sprintf("http://%s:%d", Localhost, jupyterPortInHost)
×
770
        }
×
771
        if g.GetRStudioServerConfig() != nil {
12✔
772
                res[types.ContainerLabelRStudioServerAddr] =
×
773
                        fmt.Sprintf("http://%s:%d", Localhost, rstudioServerPortInHost)
×
774
        }
×
775

776
        return res
12✔
777
}
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

© 2026 Coveralls, Inc