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
43 changes: 20 additions & 23 deletions pkg/docker/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,40 +130,41 @@ func (r *DockerHelper) ClientVersion(ctx context.Context) string {
}

// podmanMachineStartTimeout bounds a Podman machine boot, which spins up a VM.
const podmanMachineStartTimeout = 90 * time.Second
var podmanMachineStartTimeout = 90 * time.Second

var pingTimeout = 30 * time.Second

func runCmdCombined(ctx context.Context, cmd *exec.Cmd) error {
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := runCmd(ctx, cmd); err != nil {
if msg := strings.TrimSpace(out.String()); msg != "" {
return fmt.Errorf("%s: %w", msg, err)
}
return err
}
return nil
}

// Ping reports whether the runtime daemon is reachable, returning its own
// message (e.g. "Cannot connect to Podman") on failure. It runs a bare `info`
// and judges reachability by exit status: `--format` field names differ
// between docker (.ServerVersion) and podman/nerdctl, so a shared template
// would falsely fail non-docker runtimes.
func (r *DockerHelper) Ping(ctx context.Context) error {
cctx, cancel := context.WithTimeout(ctx, 10*time.Second)
cctx, cancel := context.WithTimeout(ctx, pingTimeout)
defer cancel()

out, err := r.buildCmd(cctx, "info").CombinedOutput()
if err != nil {
if msg := strings.TrimSpace(string(out)); msg != "" {
return fmt.Errorf("%s: %w", msg, err)
}
return err
}
return nil
return runCmdCombined(cctx, r.buildCmd(cctx, "info"))
}

// StartPodmanMachine starts the default Podman machine, which must already exist.
func (r *DockerHelper) StartPodmanMachine(ctx context.Context) error {
cctx, cancel := context.WithTimeout(ctx, podmanMachineStartTimeout)
defer cancel()

out, err := r.buildCmd(cctx, "machine", "start").CombinedOutput()
if err != nil {
if msg := strings.TrimSpace(string(out)); msg != "" {
return fmt.Errorf("%s: %w", msg, err)
}
return err
}
return nil
return runCmdCombined(cctx, r.buildCmd(cctx, "machine", "start"))
}

// PodmanMachineExists reports whether a Podman machine exists.
Expand Down Expand Up @@ -197,11 +198,7 @@ func (r *DockerHelper) StartRootlessPodmanSocket(ctx context.Context) error {

if runtime.GOOS == "linux" && isSystemdRunning(cctx) {
cmd := exec.CommandContext(cctx, "systemctl", "--user", "start", "podman.socket")
out, err := cmd.CombinedOutput()
if err != nil {
if msg := strings.TrimSpace(string(out)); msg != "" {
return fmt.Errorf("%s: %w", msg, err)
}
if err := runCmdCombined(cctx, cmd); err != nil {
return err
}
}
Expand Down
62 changes: 62 additions & 0 deletions pkg/docker/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,65 @@ func TestSystemdStateIsUsable(t *testing.T) {
}
assert.True(t, systemdStateIsUsable(systemdStateDegraded+"\n"))
}

func withPingTimeout(t *testing.T, d time.Duration) {
t.Helper()
original := pingTimeout
pingTimeout = d
t.Cleanup(func() { pingTimeout = original })
}

func TestPing_Succeeds(t *testing.T) {
bin := writeScript(t, t.TempDir(), "docker-fake", `#!/bin/sh
echo '{"ServerVersion":"1.0"}'
`)
h := &DockerHelper{DockerCommand: bin}
assert.NoError(t, h.Ping(context.Background()))
}

func TestPing_DaemonRefusalIsNotAttributedToTimeout(t *testing.T) {
bin := writeScript(t, t.TempDir(), "docker-fake", `#!/bin/sh
echo "Cannot connect to the Docker daemon at unix:///var/run/docker.sock" >&2
exit 1
`)
h := &DockerHelper{DockerCommand: bin}
err := h.Ping(context.Background())

require.Error(t, err)
assert.Contains(t, err.Error(), "Cannot connect to the Docker daemon")
assert.NotErrorIs(t, err, context.DeadlineExceeded,
"a fast, native refusal must not look like our own timeout")
}

func TestPing_SelfInflictedTimeoutIsDistinguishableFromDaemonDown(t *testing.T) {
withPingTimeout(t, 50*time.Millisecond)

bin := writeScript(t, t.TempDir(), "docker-fake", `#!/bin/sh
sleep 5
`)
h := &DockerHelper{DockerCommand: bin}
err := h.Ping(context.Background())

require.Error(t, err)
assert.ErrorIs(
t,
err,
context.DeadlineExceeded,
"a kill caused by Ping's own deadline must be attributable to it, not reported as a bare daemon-down error",
)
}

func TestStartPodmanMachine_SelfInflictedTimeoutIsDistinguishable(t *testing.T) {
original := podmanMachineStartTimeout
podmanMachineStartTimeout = 50 * time.Millisecond
t.Cleanup(func() { podmanMachineStartTimeout = original })

bin := writeScript(t, t.TempDir(), "docker-fake", `#!/bin/sh
sleep 5
`)
h := &DockerHelper{DockerCommand: bin}
err := h.StartPodmanMachine(context.Background())

require.Error(t, err)
assert.ErrorIs(t, err, context.DeadlineExceeded)
}
10 changes: 9 additions & 1 deletion pkg/driver/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package docker

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -213,9 +214,16 @@ func runPreflight(ctx context.Context, opts driver.PreflightOptions, p dockerPro
}
}

reachability := fmt.Sprintf("%s daemon is not reachable", p.runtime)
if errors.Is(err, context.DeadlineExceeded) {
reachability = fmt.Sprintf(
"%s daemon did not respond in time (it may just be slow to start, not necessarily down)",
p.runtime,
)
}
return &driver.PreflightError{
Provider: runtimeName,
Err: fmt.Errorf("%w: %s daemon is not reachable", err, p.runtime),
Err: fmt.Errorf("%w: %s", err, reachability),
}
}

Expand Down
29 changes: 29 additions & 0 deletions pkg/driver/docker/preflight_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package docker
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -352,3 +353,31 @@ func TestIsRootlessDockerHostEmptyFallsBackToUID(t *testing.T) {
t.Errorf("isRootlessDockerHost(\"\") = %v, want %v (uid=%d)", got, want, os.Geteuid())
}
}

func TestRunPreflightDistinguishesTimeoutFromRefusal(t *testing.T) {
timedOut := fmt.Errorf("%w: %w", context.DeadlineExceeded, errors.New("signal: killed"))
p := dockerProbe{
runtime: docker.RuntimeDocker,
lookPath: installed,
ping: func(context.Context) error { return timedOut },
}
err := runPreflight(context.Background(), driver.PreflightOptions{}, p)
require.Error(t, err)
require.ErrorIs(t, err, context.DeadlineExceeded)
require.NotContains(t, err.Error(), "is not reachable",
"a self-inflicted timeout must not be reported as an authoritative refusal")
require.Contains(t, err.Error(), "did not respond in time")
}

func TestRunPreflightDaemonRefusalKeepsUnreachableMessage(t *testing.T) {
down := errors.New("Cannot connect to the Docker daemon")
p := dockerProbe{
runtime: docker.RuntimeDocker,
lookPath: installed,
ping: func(context.Context) error { return down },
}
err := runPreflight(context.Background(), driver.PreflightOptions{}, p)
require.Error(t, err)
require.NotErrorIs(t, err, context.DeadlineExceeded)
require.Contains(t, err.Error(), "is not reachable")
}
Loading