Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 15 additions & 39 deletions pkg/docker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,23 @@ 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()
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.
// 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(
Expand Down Expand Up @@ -332,25 +342,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(
Expand Down Expand Up @@ -448,26 +444,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 {
Expand Down
144 changes: 144 additions & 0 deletions pkg/docker/runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package docker

import (
"context"
"fmt"
"os/exec"
"strings"
"sync"
"time"
)

// 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.
// 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{}, nil
case RuntimeNerdctl:
return nerdctlRuntime{}, nil
default:
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 {
mu sync.Mutex
entries map[string]ContainerRuntime
}

func (c *runtimeDetectionCache) get(dockerCommand string) ContainerRuntime {
c.mu.Lock()
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 {
ctx, cancel := context.WithTimeout(context.Background(), detectTimeout)
defer cancel()

out, err := exec.CommandContext(ctx, dockerCommand, "--version").Output()
if err != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return dockerRuntime{}
}
lower := strings.ToLower(string(out))
if strings.Contains(lower, "podman") {
return podmanRuntime{}
}
if strings.Contains(lower, "nerdctl") {
return nerdctlRuntime{}
}
return dockerRuntime{}
}
107 changes: 107 additions & 0 deletions pkg/docker/runtime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package docker

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
}{
{"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.name, func(t *testing.T) {
rt, err := RuntimeFromName(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.expected, rt.Name())
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

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, err := RuntimeFromName(string(RuntimeDocker))
require.NoError(t, err)
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, err := RuntimeFromName(string(RuntimePodman))
require.NoError(t, err)
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, err := RuntimeFromName(string(RuntimeNerdctl))
require.NoError(t, err)
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: string(RuntimeDocker),
Runtime: podmanRuntime{},
}
rt := h.GetRuntime()
assert.Equal(t, RuntimePodman, rt.Name(), "should use explicitly set runtime")
}

func TestDockerHelperIsPodmanDelegates(t *testing.T) {
h := &DockerHelper{
DockerCommand: string(RuntimeDocker),
Runtime: podmanRuntime{},
}
assert.True(t, h.IsPodman())
assert.False(t, h.IsNerdctl())
}

func TestDockerHelperIsNerdctlDelegates(t *testing.T) {
h := &DockerHelper{
DockerCommand: string(RuntimeDocker),
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")
}
8 changes: 5 additions & 3 deletions pkg/driver/docker/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}

Expand Down
Loading
Loading