Skip to content

Commit 227045c

Browse files
authored
Auto-detect Docker Desktop socket on Linux (#5122)
* Auto-detect Docker Desktop socket on Linux Docker Desktop on Linux exposes its Engine socket at $HOME/.docker/desktop/docker.sock and registers a desktop-linux Docker context pointing there. ToolHive runtime auto-detection only probed /var/run/docker.sock and macOS-specific paths, so on a Linux host with only Docker Desktop installed IsAvailable returned false and CheckRuntimeAvailable failed with "no container runtime available" -- even though docker ps in the same shell worked fine via the active context. Add the Linux Docker Desktop socket to the discovery list so auto-detection finds it without requiring TOOLHIVE_DOCKER_SOCKET. Wrap the system socket path in an unexported package variable so tests can redirect the system probe to a sandbox path. * Add unit tests for Docker socket discovery Cover three cases for Docker socket auto-detection: - discovery of the new Docker Desktop on Linux socket at $HOME/.docker/desktop/docker.sock when no system socket is present; - TOOLHIVE_DOCKER_SOCKET env override wins over filesystem discovery; - ErrRuntimeNotFound when no socket is reachable. The tests redirect the system socket probe via the unexported systemDockerSocketPath variable so they stay hermetic on machines that already have /var/run/docker.sock. * Document Docker Desktop on Linux socket path Match the documented runtime discovery order to the code by adding the Linux Docker Desktop socket path next to the existing macOS-specific entries in docs/arch/01-deployment-modes.md.
1 parent b094f75 commit 227045c

4 files changed

Lines changed: 121 additions & 4 deletions

File tree

docs/arch/01-deployment-modes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ The CLI automatically detects container runtimes in this order:
101101
- `$TOOLHIVE_DOCKER_SOCKET` (if set)
102102
- `/var/run/docker.sock`
103103
- `~/.docker/run/docker.sock` (Docker Desktop on macOS)
104+
- `~/.docker/desktop/docker.sock` (Docker Desktop on Linux)
104105
- `~/.rd/docker.sock` (Rancher Desktop on macOS)
105106
- `~/.orbstack/run/docker.sock` (OrbStack on macOS)
106107

pkg/container/docker/sdk/client_unix.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,22 @@ func findPodmanSocket() (string, error) {
166166
return "", fmt.Errorf("podman socket not found in standard locations")
167167
}
168168

169+
// systemDockerSocketPath is the system-wide Docker socket path probed by
170+
// findDockerSocket. It defaults to DockerSocketPath and is package-private so
171+
// tests can redirect the system check to a sandbox path.
172+
var systemDockerSocketPath = DockerSocketPath
173+
169174
// findDockerSocket attempts to locate a Docker socket
170175
func findDockerSocket() (string, error) {
171176
// Try Docker socket as fallback
172-
_, err := os.Stat(DockerSocketPath)
177+
_, err := os.Stat(systemDockerSocketPath)
173178

174179
if err == nil {
175-
slog.Debug("found Docker socket", "path", DockerSocketPath)
176-
return DockerSocketPath, nil
180+
slog.Debug("found Docker socket", "path", systemDockerSocketPath)
181+
return systemDockerSocketPath, nil
177182
}
178183

179-
slog.Debug("failed to check Docker socket", "path", DockerSocketPath, "error", err)
184+
slog.Debug("failed to check Docker socket", "path", systemDockerSocketPath, "error", err)
180185

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

201+
// Try Docker Desktop socket path on Linux
202+
if home := os.Getenv("HOME"); home != "" {
203+
dockerDesktopLinuxPath := filepath.Join(home, DockerDesktopLinuxSocketPath)
204+
_, err := os.Stat(dockerDesktopLinuxPath) // #nosec G703 -- path is built from HOME + constant socket path
205+
206+
if err == nil {
207+
//nolint:gosec // G706: socket path derived from HOME env var
208+
slog.Debug("found Docker Desktop socket", "path", dockerDesktopLinuxPath)
209+
return dockerDesktopLinuxPath, nil
210+
}
211+
212+
//nolint:gosec // G706: socket path derived from HOME env var
213+
slog.Debug("failed to check Docker Desktop socket", "path", dockerDesktopLinuxPath, "error", err)
214+
}
215+
196216
// Try Rancher Desktop socket path on macOS
197217
if home := os.Getenv("HOME"); home != "" {
198218
rancherDesktopPath := filepath.Join(home, RancherDesktopMacSocketPath)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build !windows
5+
6+
package sdk
7+
8+
import (
9+
"errors"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/stacklok/toolhive/pkg/container/runtime"
18+
)
19+
20+
// clearSocketEnv removes any inherited TOOLHIVE_*_SOCKET overrides so the
21+
// helpers fall through to filesystem discovery during the test.
22+
func clearSocketEnv(t *testing.T) {
23+
t.Helper()
24+
t.Setenv(PodmanSocketEnv, "")
25+
t.Setenv(DockerSocketEnv, "")
26+
t.Setenv(ColimaSocketEnv, "")
27+
}
28+
29+
// redirectSystemDockerSocket points the system-socket probe at a path that
30+
// definitely does not exist, so user-level fallbacks are exercised regardless
31+
// of whether the host has a real /var/run/docker.sock.
32+
func redirectSystemDockerSocket(t *testing.T) {
33+
t.Helper()
34+
orig := systemDockerSocketPath
35+
systemDockerSocketPath = filepath.Join(t.TempDir(), "no-such-docker.sock")
36+
t.Cleanup(func() { systemDockerSocketPath = orig })
37+
}
38+
39+
func TestFindDockerSocket_DockerDesktopOnLinux(t *testing.T) {
40+
clearSocketEnv(t)
41+
redirectSystemDockerSocket(t)
42+
43+
home := t.TempDir()
44+
t.Setenv("HOME", home)
45+
46+
socketDir := filepath.Join(home, filepath.Dir(DockerDesktopLinuxSocketPath))
47+
require.NoError(t, os.MkdirAll(socketDir, 0o755))
48+
socketPath := filepath.Join(home, DockerDesktopLinuxSocketPath)
49+
require.NoError(t, os.WriteFile(socketPath, nil, 0o600))
50+
51+
got, err := findDockerSocket()
52+
require.NoError(t, err)
53+
assert.Equal(t, socketPath, got)
54+
}
55+
56+
func TestFindPlatformContainerSocket_DockerEnvOverrideWins(t *testing.T) {
57+
clearSocketEnv(t)
58+
59+
tmp := t.TempDir()
60+
envSocket := filepath.Join(tmp, "docker-from-env.sock")
61+
require.NoError(t, os.WriteFile(envSocket, nil, 0o600))
62+
t.Setenv(DockerSocketEnv, envSocket)
63+
64+
// Even with a Docker Desktop on Linux socket present at $HOME, the env
65+
// var must take precedence.
66+
home := t.TempDir()
67+
t.Setenv("HOME", home)
68+
socketDir := filepath.Join(home, filepath.Dir(DockerDesktopLinuxSocketPath))
69+
require.NoError(t, os.MkdirAll(socketDir, 0o755))
70+
homeSocket := filepath.Join(home, DockerDesktopLinuxSocketPath)
71+
require.NoError(t, os.WriteFile(homeSocket, nil, 0o600))
72+
73+
path, rt, err := findPlatformContainerSocket(runtime.TypeDocker)
74+
require.NoError(t, err)
75+
assert.Equal(t, envSocket, path)
76+
assert.Equal(t, runtime.TypeDocker, rt)
77+
}
78+
79+
func TestFindPlatformContainerSocket_NotFound(t *testing.T) {
80+
clearSocketEnv(t)
81+
redirectSystemDockerSocket(t)
82+
83+
// Empty HOME with no sockets created — every discovery path should miss.
84+
home := t.TempDir()
85+
t.Setenv("HOME", home)
86+
t.Setenv("XDG_RUNTIME_DIR", "")
87+
t.Setenv("TMPDIR", "")
88+
89+
_, _, err := findPlatformContainerSocket(runtime.TypeDocker)
90+
require.Error(t, err)
91+
assert.True(t, errors.Is(err, ErrRuntimeNotFound), "expected ErrRuntimeNotFound, got %v", err)
92+
}

pkg/container/docker/sdk/factory.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ const (
3939
DockerSocketPath = "/var/run/docker.sock"
4040
// DockerDesktopMacSocketPath is the Docker Desktop socket path on macOS
4141
DockerDesktopMacSocketPath = ".docker/run/docker.sock"
42+
// DockerDesktopLinuxSocketPath is the Docker Desktop socket path on Linux
43+
// (relative to $HOME). Docker Desktop on Linux registers a "desktop-linux"
44+
// Docker context that points to this socket.
45+
DockerDesktopLinuxSocketPath = ".docker/desktop/docker.sock"
4246
// RancherDesktopMacSocketPath is the Docker socket path for Rancher Desktop on macOS
4347
RancherDesktopMacSocketPath = ".rd/docker.sock"
4448
// OrbStackMacSocketPath is the Docker socket path for OrbStack on macOS

0 commit comments

Comments
 (0)