From 2c354b2f882c8c2dfd937c26a92b27cc16c37b07 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 20:25:37 -0500 Subject: [PATCH 1/4] feat(runtime): add ContainerRuntime abstraction interface Replace scattered IsPodman()/IsNerdctl() runtime detection with a ContainerRuntime interface that expresses capabilities directly. Runtimes can now be explicitly configured via provider config or auto-detected from the binary with caching. --- pkg/docker/helper.go | 51 +++---------- pkg/docker/runtime.go | 131 +++++++++++++++++++++++++++++++++ pkg/docker/runtime_test.go | 96 ++++++++++++++++++++++++ pkg/driver/docker/build.go | 8 +- pkg/driver/docker/docker.go | 16 +++- pkg/provider/provider.go | 4 + providers/podman/provider.yaml | 1 + 7 files changed, 261 insertions(+), 46 deletions(-) create mode 100644 pkg/docker/runtime.go create mode 100644 pkg/docker/runtime_test.go diff --git a/pkg/docker/helper.go b/pkg/docker/helper.go index a3fd3c86c..e29c0727e 100644 --- a/pkg/docker/helper.go +++ b/pkg/docker/helper.go @@ -56,13 +56,20 @@ type DockerHelper struct { // allow command to have a custom environment Environment []string Builder DockerBuilder + Runtime ContainerRuntime } func (r *DockerHelper) GPUSupportEnabled() (bool, error) { - if r.IsPodman() { - return r.podmanGPUAvailable() + return r.GetRuntime().GPUAvailable(context.TODO(), r) +} + +// GetRuntime returns the container runtime for this helper. +// If no runtime was explicitly set, it auto-detects from the docker command. +func (r *DockerHelper) GetRuntime() ContainerRuntime { + if r.Runtime != nil { + return r.Runtime } - return r.dockerGPUAvailable() + return DetectRuntime(r.DockerCommand) } func (r *DockerHelper) FindDevContainer( @@ -332,25 +339,11 @@ func (r *DockerHelper) InspectContainers( } func (r *DockerHelper) IsPodman() bool { - args := []string{"--version"} - - out, err := r.buildCmd(context.TODO(), args...).Output() - if err != nil { - return false - } - - return strings.Contains(string(out), "podman") + return r.GetRuntime().Name() == RuntimePodman } func (r *DockerHelper) IsNerdctl() bool { - args := []string{"--version"} - - out, err := r.buildCmd(context.TODO(), args...).Output() - if err != nil { - return false - } - - return strings.Contains(string(out), "nerdctl") + return r.GetRuntime().Name() == RuntimeNerdctl } func (r *DockerHelper) Inspect( @@ -448,26 +441,6 @@ func (r *DockerHelper) GetContainerLogs( return cmd.Run() } -func (r *DockerHelper) dockerGPUAvailable() (bool, error) { - out, err := r.buildCmd(context.TODO(), "info", "-f", "{{.Runtimes.nvidia}}").Output() - if err != nil { - log.Debugf("GPU detection: docker runtime query failed, assuming no GPU: %v", err) - return false, nil - } - - return strings.Contains(string(out), "nvidia-container-runtime"), nil -} - -func (r *DockerHelper) podmanGPUAvailable() (bool, error) { - out, err := r.buildCmd(context.TODO(), "info", "-f", "{{.Host.CDIDevices}}").Output() - if err != nil { - log.Debugf("GPU detection: podman CDI query failed, assuming no GPU: %v", err) - return false, nil - } - - return strings.Contains(strings.ToLower(string(out)), "nvidia"), nil -} - func (r *DockerHelper) buildCmd(ctx context.Context, args ...string) *exec.Cmd { cmd := exec.CommandContext(ctx, r.DockerCommand, args...) if r.Environment != nil { diff --git a/pkg/docker/runtime.go b/pkg/docker/runtime.go new file mode 100644 index 000000000..961e00493 --- /dev/null +++ b/pkg/docker/runtime.go @@ -0,0 +1,131 @@ +package docker + +import ( + "context" + "os/exec" + "strings" + "sync" +) + +// RuntimeName identifies a container runtime. +type RuntimeName string + +const ( + RuntimeDocker RuntimeName = "docker" + RuntimePodman RuntimeName = "podman" + RuntimeNerdctl RuntimeName = "nerdctl" +) + +// ContainerRuntime describes the capabilities of a container runtime (Docker, Podman, nerdctl). +// Rather than checking IsPodman()/IsNerdctl() at every call site, callers query capability +// methods that express *why* the code branches. +type ContainerRuntime interface { + Name() RuntimeName + SupportsInternalBuildKit() bool + SupportsSignalProxy() bool + SupportsMountConsistency() bool + NeedsUserNamespaceArgs() bool + GPUAvailable(ctx context.Context, helper *DockerHelper) (bool, error) +} + +type dockerRuntime struct{} + +func (dockerRuntime) Name() RuntimeName { return RuntimeDocker } +func (dockerRuntime) SupportsInternalBuildKit() bool { return true } +func (dockerRuntime) SupportsSignalProxy() bool { return true } +func (dockerRuntime) SupportsMountConsistency() bool { return true } +func (dockerRuntime) NeedsUserNamespaceArgs() bool { return false } + +func (dockerRuntime) GPUAvailable(ctx context.Context, h *DockerHelper) (bool, error) { + out, err := h.buildCmd(ctx, "info", "-f", "{{.Runtimes.nvidia}}").Output() + if err != nil { + return false, nil + } + return strings.Contains(string(out), "nvidia-container-runtime"), nil +} + +type podmanRuntime struct{} + +func (podmanRuntime) Name() RuntimeName { return RuntimePodman } +func (podmanRuntime) SupportsInternalBuildKit() bool { return false } +func (podmanRuntime) SupportsSignalProxy() bool { return true } +func (podmanRuntime) SupportsMountConsistency() bool { return true } +func (podmanRuntime) NeedsUserNamespaceArgs() bool { return true } + +func (podmanRuntime) GPUAvailable(ctx context.Context, h *DockerHelper) (bool, error) { + out, err := h.buildCmd(ctx, "info", "-f", "{{.Host.CDIDevices}}").Output() + if err != nil { + return false, nil + } + return strings.Contains(strings.ToLower(string(out)), "nvidia"), nil +} + +type nerdctlRuntime struct{} + +func (nerdctlRuntime) Name() RuntimeName { return RuntimeNerdctl } +func (nerdctlRuntime) SupportsInternalBuildKit() bool { return true } +func (nerdctlRuntime) SupportsSignalProxy() bool { return false } +func (nerdctlRuntime) SupportsMountConsistency() bool { return false } +func (nerdctlRuntime) NeedsUserNamespaceArgs() bool { return false } + +func (nerdctlRuntime) GPUAvailable(ctx context.Context, h *DockerHelper) (bool, error) { + out, err := h.buildCmd(ctx, "info", "-f", "{{.Runtimes.nvidia}}").Output() + if err != nil { + return false, nil + } + return strings.Contains(string(out), "nvidia-container-runtime"), nil +} + +// DetectRuntime probes the binary at dockerCommand to determine which runtime it is. +// The result is cached per binary path. +func DetectRuntime(dockerCommand string) ContainerRuntime { + return runtimeCache.get(dockerCommand) +} + +// RuntimeFromName returns a ContainerRuntime for the given explicit name. +// Falls back to Docker for unrecognized values. +func RuntimeFromName(name string) ContainerRuntime { + switch RuntimeName(strings.ToLower(name)) { + case RuntimePodman: + return podmanRuntime{} + case RuntimeNerdctl: + return nerdctlRuntime{} + default: + return dockerRuntime{} + } +} + +var runtimeCache = &runtimeDetectionCache{entries: make(map[string]ContainerRuntime)} + +type runtimeDetectionCache struct { + mu sync.Mutex + entries map[string]ContainerRuntime +} + +func (c *runtimeDetectionCache) get(dockerCommand string) ContainerRuntime { + c.mu.Lock() + defer c.mu.Unlock() + + if rt, ok := c.entries[dockerCommand]; ok { + return rt + } + + rt := detect(dockerCommand) + c.entries[dockerCommand] = rt + return rt +} + +func detect(dockerCommand string) ContainerRuntime { + out, err := exec.Command(dockerCommand, "--version").Output() + if err != nil { + return dockerRuntime{} + } + lower := strings.ToLower(string(out)) + if strings.Contains(lower, "podman") { + return podmanRuntime{} + } + if strings.Contains(lower, "nerdctl") { + return nerdctlRuntime{} + } + return dockerRuntime{} +} diff --git a/pkg/docker/runtime_test.go b/pkg/docker/runtime_test.go new file mode 100644 index 000000000..04619a747 --- /dev/null +++ b/pkg/docker/runtime_test.go @@ -0,0 +1,96 @@ +package docker + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRuntimeFromName(t *testing.T) { + tests := []struct { + input string + expected RuntimeName + }{ + {"docker", RuntimeDocker}, + {"podman", RuntimePodman}, + {"nerdctl", RuntimeNerdctl}, + {"Docker", RuntimeDocker}, + {"Podman", RuntimePodman}, + {"NERDCTL", RuntimeNerdctl}, + {"", RuntimeDocker}, + {"unknown", RuntimeDocker}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + rt := RuntimeFromName(tt.input) + assert.Equal(t, tt.expected, rt.Name()) + }) + } +} + +func TestDockerRuntimeCapabilities(t *testing.T) { + rt := RuntimeFromName("docker") + assert.Equal(t, RuntimeDocker, rt.Name()) + assert.True(t, rt.SupportsInternalBuildKit()) + assert.True(t, rt.SupportsSignalProxy()) + assert.True(t, rt.SupportsMountConsistency()) + assert.False(t, rt.NeedsUserNamespaceArgs()) +} + +func TestPodmanRuntimeCapabilities(t *testing.T) { + rt := RuntimeFromName("podman") + assert.Equal(t, RuntimePodman, rt.Name()) + assert.False(t, rt.SupportsInternalBuildKit()) + assert.True(t, rt.SupportsSignalProxy()) + assert.True(t, rt.SupportsMountConsistency()) + assert.True(t, rt.NeedsUserNamespaceArgs()) +} + +func TestNerdctlRuntimeCapabilities(t *testing.T) { + rt := RuntimeFromName("nerdctl") + assert.Equal(t, RuntimeNerdctl, rt.Name()) + assert.True(t, rt.SupportsInternalBuildKit()) + assert.False(t, rt.SupportsSignalProxy()) + assert.False(t, rt.SupportsMountConsistency()) + assert.False(t, rt.NeedsUserNamespaceArgs()) +} + +func TestDockerHelperGetRuntimeFallback(t *testing.T) { + h := &DockerHelper{DockerCommand: "nonexistent-binary-xyz"} + rt := h.GetRuntime() + assert.Equal(t, RuntimeDocker, rt.Name(), "should fall back to docker for unknown binary") +} + +func TestDockerHelperGetRuntimeExplicit(t *testing.T) { + h := &DockerHelper{ + DockerCommand: "docker", + Runtime: podmanRuntime{}, + } + rt := h.GetRuntime() + assert.Equal(t, RuntimePodman, rt.Name(), "should use explicitly set runtime") +} + +func TestDockerHelperIsPodmanDelegates(t *testing.T) { + h := &DockerHelper{ + DockerCommand: "docker", + Runtime: podmanRuntime{}, + } + assert.True(t, h.IsPodman()) + assert.False(t, h.IsNerdctl()) +} + +func TestDockerHelperIsNerdctlDelegates(t *testing.T) { + h := &DockerHelper{ + DockerCommand: "docker", + Runtime: nerdctlRuntime{}, + } + assert.False(t, h.IsPodman()) + assert.True(t, h.IsNerdctl()) +} + +func TestDetectRuntimeCaching(t *testing.T) { + rt1 := DetectRuntime("nonexistent-binary-abc") + rt2 := DetectRuntime("nonexistent-binary-abc") + assert.Equal(t, rt1, rt2, "same binary should return same cached runtime") +} diff --git a/pkg/driver/docker/build.go b/pkg/driver/docker/build.go index 91c6d95e4..9df78524e 100644 --- a/pkg/driver/docker/build.go +++ b/pkg/driver/docker/build.go @@ -272,9 +272,11 @@ type buildOrchestrator struct { } func (o *buildOrchestrator) selectStrategy(options provider.BuildOptions) buildStrategy { - // Podman doesn't support the Docker BuildKit session API, so force CLI build. - if o.driver.Docker.IsPodman() { - log.Debugf("podman detected, forcing docker buildx strategy") + if !o.driver.Docker.GetRuntime().SupportsInternalBuildKit() { + log.Debugf( + "%s detected, forcing docker buildx strategy", + o.driver.Docker.GetRuntime().Name(), + ) return &dockerBuildxStrategy{driver: o.driver} } diff --git a/pkg/driver/docker/docker.go b/pkg/driver/docker/docker.go index fb84e0893..8ff008cf9 100644 --- a/pkg/driver/docker/docker.go +++ b/pkg/driver/docker/docker.go @@ -49,13 +49,21 @@ func NewDockerDriver( return nil, err } - log.Debugf("using docker command: command=%s", dockerCommand) + var rt docker.ContainerRuntime + if workspaceInfo.Agent.Docker.Runtime != "" { + rt = docker.RuntimeFromName(workspaceInfo.Agent.Docker.Runtime) + } else { + rt = docker.DetectRuntime(dockerCommand) + } + + log.Debugf("using docker command: command=%s, runtime=%s", dockerCommand, rt.Name()) return &dockerDriver{ Docker: &docker.DockerHelper{ DockerCommand: dockerCommand, Environment: makeEnvironment(workspaceInfo.Agent.Docker.Env), ContainerID: workspaceInfo.Workspace.Source.Container, Builder: builder, + Runtime: rt, }, IDLabels: workspaceInfo.CLIOptions.IDLabels, UpdateRemoteUserUIDDefault: workspaceInfo.CLIOptions.UpdateRemoteUserUIDDefault, @@ -482,7 +490,7 @@ func (d *dockerDriver) buildRunArgs( params: params, } - if !helper.IsNerdctl() { + if helper.GetRuntime().SupportsSignalProxy() { b.args = append(b.args, "--sig-proxy=false") } @@ -626,7 +634,7 @@ func (d *dockerDriver) addWorkspaceMountArgs( if options.WorkspaceMount != nil { workspacePath := d.EnsurePath(options.WorkspaceMount) mountPath := workspacePath.String() - if helper.IsNerdctl() { + if !helper.GetRuntime().SupportsMountConsistency() { mountPath = stripMountConsistency(mountPath) } args = append(args, "--mount", mountPath) @@ -825,7 +833,7 @@ func (d *dockerDriver) getPodmanArgs( options *driver.RunOptions, parsedConfig *config.DevContainerConfig, ) ([]string, error) { - if !d.Docker.IsPodman() { + if !d.Docker.GetRuntime().NeedsUserNamespaceArgs() { return []string{}, nil } diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 845333bae..8580ebe0d 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -198,6 +198,10 @@ type ProviderDockerDriverConfig struct { // HelperImage is used by LocalDockerDelivery for volume population. // When empty, defaults to busybox:latest. A direct-copy fallback is used if the helper container approach fails. HelperImage string `json:"helperImage,omitempty"` + + // Runtime identifies the container runtime explicitly (docker, podman, nerdctl). + // When empty, the runtime is auto-detected from the binary at Path. + Runtime string `json:"runtime,omitempty"` } type ProviderKubernetesDriverConfig struct { diff --git a/providers/podman/provider.yaml b/providers/podman/provider.yaml index b28845b61..15f1adc28 100644 --- a/providers/podman/provider.yaml +++ b/providers/podman/provider.yaml @@ -25,6 +25,7 @@ agent: docker: path: ${PODMAN_PATH} install: false + runtime: podman env: DOCKER_HOST: ${PODMAN_HOST} exec: From 2c6113b9470d0f1947a8fb1483093cddf39f7c85 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 20:33:19 -0500 Subject: [PATCH 2/4] fix(lint): use runtime constants instead of string literals in tests --- pkg/docker/runtime_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/docker/runtime_test.go b/pkg/docker/runtime_test.go index 04619a747..c28746636 100644 --- a/pkg/docker/runtime_test.go +++ b/pkg/docker/runtime_test.go @@ -11,9 +11,9 @@ func TestRuntimeFromName(t *testing.T) { input string expected RuntimeName }{ - {"docker", RuntimeDocker}, - {"podman", RuntimePodman}, - {"nerdctl", RuntimeNerdctl}, + {string(RuntimeDocker), RuntimeDocker}, + {string(RuntimePodman), RuntimePodman}, + {string(RuntimeNerdctl), RuntimeNerdctl}, {"Docker", RuntimeDocker}, {"Podman", RuntimePodman}, {"NERDCTL", RuntimeNerdctl}, @@ -30,7 +30,7 @@ func TestRuntimeFromName(t *testing.T) { } func TestDockerRuntimeCapabilities(t *testing.T) { - rt := RuntimeFromName("docker") + rt := RuntimeFromName(string(RuntimeDocker)) assert.Equal(t, RuntimeDocker, rt.Name()) assert.True(t, rt.SupportsInternalBuildKit()) assert.True(t, rt.SupportsSignalProxy()) @@ -39,7 +39,7 @@ func TestDockerRuntimeCapabilities(t *testing.T) { } func TestPodmanRuntimeCapabilities(t *testing.T) { - rt := RuntimeFromName("podman") + rt := RuntimeFromName(string(RuntimePodman)) assert.Equal(t, RuntimePodman, rt.Name()) assert.False(t, rt.SupportsInternalBuildKit()) assert.True(t, rt.SupportsSignalProxy()) @@ -48,7 +48,7 @@ func TestPodmanRuntimeCapabilities(t *testing.T) { } func TestNerdctlRuntimeCapabilities(t *testing.T) { - rt := RuntimeFromName("nerdctl") + rt := RuntimeFromName(string(RuntimeNerdctl)) assert.Equal(t, RuntimeNerdctl, rt.Name()) assert.True(t, rt.SupportsInternalBuildKit()) assert.False(t, rt.SupportsSignalProxy()) From eb8db47936b041e0ddf394197e595347263177fa Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 20:39:01 -0500 Subject: [PATCH 3/4] fix(lint): replace remaining string literals with runtime constants --- pkg/docker/runtime_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/docker/runtime_test.go b/pkg/docker/runtime_test.go index c28746636..58436c0c6 100644 --- a/pkg/docker/runtime_test.go +++ b/pkg/docker/runtime_test.go @@ -64,7 +64,7 @@ func TestDockerHelperGetRuntimeFallback(t *testing.T) { func TestDockerHelperGetRuntimeExplicit(t *testing.T) { h := &DockerHelper{ - DockerCommand: "docker", + DockerCommand: string(RuntimeDocker), Runtime: podmanRuntime{}, } rt := h.GetRuntime() @@ -73,7 +73,7 @@ func TestDockerHelperGetRuntimeExplicit(t *testing.T) { func TestDockerHelperIsPodmanDelegates(t *testing.T) { h := &DockerHelper{ - DockerCommand: "docker", + DockerCommand: string(RuntimeDocker), Runtime: podmanRuntime{}, } assert.True(t, h.IsPodman()) @@ -82,7 +82,7 @@ func TestDockerHelperIsPodmanDelegates(t *testing.T) { func TestDockerHelperIsNerdctlDelegates(t *testing.T) { h := &DockerHelper{ - DockerCommand: "docker", + DockerCommand: string(RuntimeDocker), Runtime: nerdctlRuntime{}, } assert.False(t, h.IsPodman()) From 18afe35b18958014b9cb274860ec0d18d982ebd7 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Wed, 13 May 2026 20:44:43 -0500 Subject: [PATCH 4/4] fix(runtime): address code review feedback - Add 5s timeout to GPUSupportEnabled() instead of context.TODO() - Add 5s timeout to detect() runtime probe via exec.CommandContext - Restructure runtimeDetectionCache to release mutex during exec - Make RuntimeFromName return error for unknown runtime names - Fix empty subtest name in RuntimeFromName test table - Add test for unknown runtime name error case --- pkg/docker/helper.go | 5 ++++- pkg/docker/runtime.go | 31 ++++++++++++++++++++++--------- pkg/docker/runtime_test.go | 37 ++++++++++++++++++++++++------------- pkg/driver/docker/docker.go | 6 +++++- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/pkg/docker/helper.go b/pkg/docker/helper.go index e29c0727e..d11bc0668 100644 --- a/pkg/docker/helper.go +++ b/pkg/docker/helper.go @@ -60,7 +60,10 @@ type DockerHelper struct { } func (r *DockerHelper) GPUSupportEnabled() (bool, error) { - return r.GetRuntime().GPUAvailable(context.TODO(), r) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + return r.GetRuntime().GPUAvailable(ctx, r) } // GetRuntime returns the container runtime for this helper. diff --git a/pkg/docker/runtime.go b/pkg/docker/runtime.go index 961e00493..376d6476a 100644 --- a/pkg/docker/runtime.go +++ b/pkg/docker/runtime.go @@ -2,9 +2,11 @@ package docker import ( "context" + "fmt" "os/exec" "strings" "sync" + "time" ) // RuntimeName identifies a container runtime. @@ -83,18 +85,23 @@ func DetectRuntime(dockerCommand string) ContainerRuntime { } // RuntimeFromName returns a ContainerRuntime for the given explicit name. -// Falls back to Docker for unrecognized values. -func RuntimeFromName(name string) ContainerRuntime { - switch RuntimeName(strings.ToLower(name)) { +// Empty string defaults to Docker. Unknown non-empty names return an error +// to catch typos in provider configuration. +func RuntimeFromName(name string) (ContainerRuntime, error) { + switch RuntimeName(strings.ToLower(strings.TrimSpace(name))) { + case "", RuntimeDocker: + return dockerRuntime{}, nil case RuntimePodman: - return podmanRuntime{} + return podmanRuntime{}, nil case RuntimeNerdctl: - return nerdctlRuntime{} + return nerdctlRuntime{}, nil default: - return dockerRuntime{} + return nil, fmt.Errorf("unknown container runtime %q", name) } } +const detectTimeout = 5 * time.Second + var runtimeCache = &runtimeDetectionCache{entries: make(map[string]ContainerRuntime)} type runtimeDetectionCache struct { @@ -104,19 +111,25 @@ type runtimeDetectionCache struct { func (c *runtimeDetectionCache) get(dockerCommand string) ContainerRuntime { c.mu.Lock() - defer c.mu.Unlock() - if rt, ok := c.entries[dockerCommand]; ok { + c.mu.Unlock() return rt } + c.mu.Unlock() rt := detect(dockerCommand) + + c.mu.Lock() c.entries[dockerCommand] = rt + c.mu.Unlock() return rt } func detect(dockerCommand string) ContainerRuntime { - out, err := exec.Command(dockerCommand, "--version").Output() + ctx, cancel := context.WithTimeout(context.Background(), detectTimeout) + defer cancel() + + out, err := exec.CommandContext(ctx, dockerCommand, "--version").Output() if err != nil { return dockerRuntime{} } diff --git a/pkg/docker/runtime_test.go b/pkg/docker/runtime_test.go index 58436c0c6..f399eb6cb 100644 --- a/pkg/docker/runtime_test.go +++ b/pkg/docker/runtime_test.go @@ -4,33 +4,42 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRuntimeFromName(t *testing.T) { tests := []struct { + name string input string expected RuntimeName }{ - {string(RuntimeDocker), RuntimeDocker}, - {string(RuntimePodman), RuntimePodman}, - {string(RuntimeNerdctl), RuntimeNerdctl}, - {"Docker", RuntimeDocker}, - {"Podman", RuntimePodman}, - {"NERDCTL", RuntimeNerdctl}, - {"", RuntimeDocker}, - {"unknown", RuntimeDocker}, + {"docker", string(RuntimeDocker), RuntimeDocker}, + {"podman", string(RuntimePodman), RuntimePodman}, + {"nerdctl", string(RuntimeNerdctl), RuntimeNerdctl}, + {"Docker-uppercase", "Docker", RuntimeDocker}, + {"Podman-uppercase", "Podman", RuntimePodman}, + {"NERDCTL-uppercase", "NERDCTL", RuntimeNerdctl}, + {"empty", "", RuntimeDocker}, } for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - rt := RuntimeFromName(tt.input) + t.Run(tt.name, func(t *testing.T) { + rt, err := RuntimeFromName(tt.input) + require.NoError(t, err) assert.Equal(t, tt.expected, rt.Name()) }) } } +func TestRuntimeFromNameUnknown(t *testing.T) { + _, err := RuntimeFromName("unknown") + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown container runtime") +} + func TestDockerRuntimeCapabilities(t *testing.T) { - rt := RuntimeFromName(string(RuntimeDocker)) + rt, err := RuntimeFromName(string(RuntimeDocker)) + require.NoError(t, err) assert.Equal(t, RuntimeDocker, rt.Name()) assert.True(t, rt.SupportsInternalBuildKit()) assert.True(t, rt.SupportsSignalProxy()) @@ -39,7 +48,8 @@ func TestDockerRuntimeCapabilities(t *testing.T) { } func TestPodmanRuntimeCapabilities(t *testing.T) { - rt := RuntimeFromName(string(RuntimePodman)) + rt, err := RuntimeFromName(string(RuntimePodman)) + require.NoError(t, err) assert.Equal(t, RuntimePodman, rt.Name()) assert.False(t, rt.SupportsInternalBuildKit()) assert.True(t, rt.SupportsSignalProxy()) @@ -48,7 +58,8 @@ func TestPodmanRuntimeCapabilities(t *testing.T) { } func TestNerdctlRuntimeCapabilities(t *testing.T) { - rt := RuntimeFromName(string(RuntimeNerdctl)) + rt, err := RuntimeFromName(string(RuntimeNerdctl)) + require.NoError(t, err) assert.Equal(t, RuntimeNerdctl, rt.Name()) assert.True(t, rt.SupportsInternalBuildKit()) assert.False(t, rt.SupportsSignalProxy()) diff --git a/pkg/driver/docker/docker.go b/pkg/driver/docker/docker.go index 8ff008cf9..43fbf6033 100644 --- a/pkg/driver/docker/docker.go +++ b/pkg/driver/docker/docker.go @@ -51,7 +51,11 @@ func NewDockerDriver( var rt docker.ContainerRuntime if workspaceInfo.Agent.Docker.Runtime != "" { - rt = docker.RuntimeFromName(workspaceInfo.Agent.Docker.Runtime) + var err error + rt, err = docker.RuntimeFromName(workspaceInfo.Agent.Docker.Runtime) + if err != nil { + return nil, fmt.Errorf("invalid runtime config: %w", err) + } } else { rt = docker.DetectRuntime(dockerCommand) }