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
1 change: 1 addition & 0 deletions docs/arch/01-deployment-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ The CLI automatically detects container runtimes in this order:
- `$TOOLHIVE_DOCKER_SOCKET` (if set)
- `/var/run/docker.sock`
- `~/.docker/run/docker.sock` (Docker Desktop on macOS)
- `~/.docker/desktop/docker.sock` (Docker Desktop on Linux)
- `~/.rd/docker.sock` (Rancher Desktop on macOS)
- `~/.orbstack/run/docker.sock` (OrbStack on macOS)

Expand Down
28 changes: 24 additions & 4 deletions pkg/container/docker/sdk/client_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,22 @@ func findPodmanSocket() (string, error) {
return "", fmt.Errorf("podman socket not found in standard locations")
}

// systemDockerSocketPath is the system-wide Docker socket path probed by
// findDockerSocket. It defaults to DockerSocketPath and is package-private so
// tests can redirect the system check to a sandbox path.
var systemDockerSocketPath = DockerSocketPath

// findDockerSocket attempts to locate a Docker socket
func findDockerSocket() (string, error) {
// Try Docker socket as fallback
_, err := os.Stat(DockerSocketPath)
_, err := os.Stat(systemDockerSocketPath)

if err == nil {
slog.Debug("found Docker socket", "path", DockerSocketPath)
return DockerSocketPath, nil
slog.Debug("found Docker socket", "path", systemDockerSocketPath)
return systemDockerSocketPath, nil
}

slog.Debug("failed to check Docker socket", "path", DockerSocketPath, "error", err)
slog.Debug("failed to check Docker socket", "path", systemDockerSocketPath, "error", err)

// Try Docker Desktop socket path on macOS
if home := os.Getenv("HOME"); home != "" {
Expand All @@ -193,6 +198,21 @@ func findDockerSocket() (string, error) {
slog.Debug("failed to check Docker Desktop socket", "path", dockerDesktopPath, "error", err)
}

// Try Docker Desktop socket path on Linux
if home := os.Getenv("HOME"); home != "" {
dockerDesktopLinuxPath := filepath.Join(home, DockerDesktopLinuxSocketPath)
_, err := os.Stat(dockerDesktopLinuxPath) // #nosec G703 -- path is built from HOME + constant socket path

if err == nil {
//nolint:gosec // G706: socket path derived from HOME env var
slog.Debug("found Docker Desktop socket", "path", dockerDesktopLinuxPath)
return dockerDesktopLinuxPath, nil
}

//nolint:gosec // G706: socket path derived from HOME env var
slog.Debug("failed to check Docker Desktop socket", "path", dockerDesktopLinuxPath, "error", err)
}

// Try Rancher Desktop socket path on macOS
if home := os.Getenv("HOME"); home != "" {
rancherDesktopPath := filepath.Join(home, RancherDesktopMacSocketPath)
Expand Down
92 changes: 92 additions & 0 deletions pkg/container/docker/sdk/client_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
// SPDX-License-Identifier: Apache-2.0

//go:build !windows

package sdk

import (
"errors"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/stacklok/toolhive/pkg/container/runtime"
)

// clearSocketEnv removes any inherited TOOLHIVE_*_SOCKET overrides so the
// helpers fall through to filesystem discovery during the test.
func clearSocketEnv(t *testing.T) {
t.Helper()
t.Setenv(PodmanSocketEnv, "")
t.Setenv(DockerSocketEnv, "")
t.Setenv(ColimaSocketEnv, "")
}

// redirectSystemDockerSocket points the system-socket probe at a path that
// definitely does not exist, so user-level fallbacks are exercised regardless
// of whether the host has a real /var/run/docker.sock.
func redirectSystemDockerSocket(t *testing.T) {
t.Helper()
orig := systemDockerSocketPath
systemDockerSocketPath = filepath.Join(t.TempDir(), "no-such-docker.sock")
t.Cleanup(func() { systemDockerSocketPath = orig })
}

func TestFindDockerSocket_DockerDesktopOnLinux(t *testing.T) {
clearSocketEnv(t)
redirectSystemDockerSocket(t)

home := t.TempDir()
t.Setenv("HOME", home)

socketDir := filepath.Join(home, filepath.Dir(DockerDesktopLinuxSocketPath))
require.NoError(t, os.MkdirAll(socketDir, 0o755))
socketPath := filepath.Join(home, DockerDesktopLinuxSocketPath)
require.NoError(t, os.WriteFile(socketPath, nil, 0o600))

got, err := findDockerSocket()
require.NoError(t, err)
assert.Equal(t, socketPath, got)
}

func TestFindPlatformContainerSocket_DockerEnvOverrideWins(t *testing.T) {
clearSocketEnv(t)

tmp := t.TempDir()
envSocket := filepath.Join(tmp, "docker-from-env.sock")
require.NoError(t, os.WriteFile(envSocket, nil, 0o600))
t.Setenv(DockerSocketEnv, envSocket)

// Even with a Docker Desktop on Linux socket present at $HOME, the env
// var must take precedence.
home := t.TempDir()
t.Setenv("HOME", home)
socketDir := filepath.Join(home, filepath.Dir(DockerDesktopLinuxSocketPath))
require.NoError(t, os.MkdirAll(socketDir, 0o755))
homeSocket := filepath.Join(home, DockerDesktopLinuxSocketPath)
require.NoError(t, os.WriteFile(homeSocket, nil, 0o600))

path, rt, err := findPlatformContainerSocket(runtime.TypeDocker)
require.NoError(t, err)
assert.Equal(t, envSocket, path)
assert.Equal(t, runtime.TypeDocker, rt)
}

func TestFindPlatformContainerSocket_NotFound(t *testing.T) {
clearSocketEnv(t)
redirectSystemDockerSocket(t)

// Empty HOME with no sockets created — every discovery path should miss.
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_RUNTIME_DIR", "")
t.Setenv("TMPDIR", "")

_, _, err := findPlatformContainerSocket(runtime.TypeDocker)
require.Error(t, err)
assert.True(t, errors.Is(err, ErrRuntimeNotFound), "expected ErrRuntimeNotFound, got %v", err)
}
4 changes: 4 additions & 0 deletions pkg/container/docker/sdk/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const (
DockerSocketPath = "/var/run/docker.sock"
// DockerDesktopMacSocketPath is the Docker Desktop socket path on macOS
DockerDesktopMacSocketPath = ".docker/run/docker.sock"
// DockerDesktopLinuxSocketPath is the Docker Desktop socket path on Linux
// (relative to $HOME). Docker Desktop on Linux registers a "desktop-linux"
// Docker context that points to this socket.
DockerDesktopLinuxSocketPath = ".docker/desktop/docker.sock"
// RancherDesktopMacSocketPath is the Docker socket path for Rancher Desktop on macOS
RancherDesktopMacSocketPath = ".rd/docker.sock"
// OrbStackMacSocketPath is the Docker socket path for OrbStack on macOS
Expand Down
Loading