Allow container socket paths to be configured via config file#4943
Allow container socket paths to be configured via config file#4943
Conversation
|
@claude deep review of branch please |
|
Claude finished @kantord's task in 2m 27s —— View job Deep Code Review
Summary: This PR adds support for configuring container socket paths (Docker, Podman, Colima) via the ToolHive config file ( 🔴 Critical — Build broken on Windows
func findPlatformContainerSocket(rt runtime.Type) (string, runtime.Type, error) {But return findPlatformContainerSocket(rt, overrides)This is a compile error on Windows. The Windows implementation needs to be updated to accept 🔴 High — Config overrides ignore the
|
| Severity | Issue |
|---|---|
| 🔴 Build-breaking | Windows compile error — client_windows.go not updated to new signature |
| 🔴 Runtime bug | Config overrides don't gate on rt, blocking runtime fallback |
| 🟡 Usability | container_runtime fields missing from main Config struct |
| 🟡 Correctness | loadSocketOverrides ignores custom --config path |
| 🟡 Maintainability | Parallel config type risks drifting from main schema |
| 🟡 Correctness | Colima override returns TypeDocker vs TypeColima in auto-detection |
| 🟡 Test coverage | No tests for Podman/Colima overrides, invalid paths, or rt-mismatch cases |
| 🟢 Cosmetic | makeTempSocket creates a regular file, not a socket |
| 🟢 Process | PR description is empty |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4943 +/- ##
==========================================
+ Coverage 69.49% 69.54% +0.04%
==========================================
Files 551 551
Lines 55907 55938 +31
==========================================
+ Hits 38854 38902 +48
+ Misses 14058 14039 -19
- Partials 2995 2997 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
jhrozek
left a comment
There was a problem hiding this comment.
A few observations from reviewing the socket-override threading.
| return customSocketPath, runtime.TypeDocker, nil | ||
| } | ||
|
|
||
| // Check config file overrides (after env vars, before auto-detection) |
There was a problem hiding this comment.
I think there's a correctness issue here. checkSocketConfigOverrides doesn't consider the rt argument, so whichever override field is non-empty wins for every iteration of the outer loop in NewDockerClient.
Concrete scenario: a user has a stale podman_socket: in config and Docker is reachable via auto-detect. Iter 1 (Podman) fails because the Podman path doesn't exist. Iter 2 should auto-detect Docker — but checkSocketConfigOverrides returns the same Podman error again, Docker auto-detect never runs, and NewDockerClient returns "no supported container runtime available: invalid Podman socket path from config" on a machine where Docker works fine.
Windows has the same shape at client_windows.go:92-99.
Fix I'd suggest: make the override check match the current rt — only look at overrides.PodmanSocket when rt == TypePodman, etc. Then a broken podman override only disables podman, and the outer loop's independent-per-runtime semantics work as intended. Side benefit: the "priority ordering" question (which field wins when multiple are set) disappears, since each runtime consults only its own field.
|
|
||
| // loadSocketOverrides reads socket path overrides from the ToolHive config file. | ||
| // Best-effort: returns empty overrides on any error so auto-detection takes over. | ||
| func loadSocketOverrides() runtime.SocketConfig { |
There was a problem hiding this comment.
Would it be worth routing this through the existing config Provider abstraction? pkg/config.NewProvider() dispatches to DefaultProvider / PathProvider / KubernetesProvider and honours RegisterProviderFactory for injected providers (see pkg/config/interface.go:225, :396, :566). Reading XDG directly means PathProvider consumers (custom config paths, tests) and any code using RegisterProviderFactory won't see this feature.
The reason to not just use NewProvider().GetConfig() is the "first-run creates the file + runs migrations" side effect — but in practice the CLI has almost always touched config by the time NewDockerClient runs, so the singleton is populated and no extra file creation happens. A read-only LoadIfExists() on the Provider interface would make the concern go away entirely.
| return resolveConfigSocket(overrides.DockerSocket, runtime.TypeDocker, "Docker") | ||
| } | ||
| if overrides.ColimaSocket != "" { | ||
| return resolveConfigSocket(overrides.ColimaSocket, runtime.TypeDocker, "Colima") |
There was a problem hiding this comment.
Small inconsistency: the config override for Colima returns runtime.TypeDocker here, but the auto-detect branch at client_unix.go:98-103 returns runtime.TypeColima. The env-var branch at client_unix.go:74-76 also returns TypeDocker, so this PR is copying the existing pattern — but the disagreement with auto-detect means any caller that branches on runtime.TypeColima behaves differently depending on how the user configured Colima.
Either align all three branches (env + config to return TypeColima, or auto-detect to TypeDocker), or leave a comment explaining why the three disagree.
| var cfg struct { | ||
| ContainerRuntime runtime.SocketConfig `yaml:"container_runtime,omitempty"` | ||
| } | ||
| if err := yaml.Unmarshal(data, &cfg); err != nil { |
There was a problem hiding this comment.
loadSocketOverrides has no tests, and it's the integration point that turns YAML into SocketConfig. It also has a deliberate "best-effort, return zero value on any error" contract across three distinct failure modes (xdg lookup, file read, yaml parse) — that contract should be pinned.
Worth a table-driven test that points XDG_CONFIG_HOME at t.TempDir() and covers: missing file, empty file, malformed YAML, valid file with each subset of fields populated, valid file with no container_runtime key, valid file with only unrelated keys. adrg/xdg provides xdg.Reload() if you hit caching issues on macOS. Non-parallel because of t.Setenv.
Summary
Users with non-standard container socket paths had to set environment variables
(
TOOLHIVE_DOCKER_SOCKET,TOOLHIVE_PODMAN_SOCKET,TOOLHIVE_COLIMA_SOCKET) in everyshell session or wrapper script. This is fragile because GUI and CLI processes can inherit
environment variables differently depending on how they are launched, making misconfiguration
hard to spot and debug.
container_runtimeblock to~/.config/toolhive/config.yamlso socket paths can beset once and persist across all launch methods
SocketConfigtopkg/container/runtimeas the shared type used by both the configpackage and the socket detection code (avoids an import cycle)
Fixes #4612
Type of change
Test plan
task test)task lint-fix)New unit tests in
pkg/container/docker/sdk/client_unix_test.go:TestFindContainerSocket_ConfigPath— table-driven: valid config path accepted, nonexistent path returns errorTestFindContainerSocket_EnvVarPrecedence— env var overrides a config-supplied pathDoes this introduce a user-facing change?
Yes. Users can now configure socket paths in
~/.config/toolhive/config.yaml:This is lower-friction than setting environment variables, and works consistently regardless of how ToolHive is launched.
Special notes for reviewers
The
SocketConfigstruct lives inpkg/container/runtimerather thanpkg/configto avoidan import cycle:
pkg/container/docker/sdk→pkg/config→pkg/secrets→ ... →pkg/container/docker/sdk.The
pkg/container/runtimepackage is already imported by both sides and has precedent forthis pattern (see
ScalingConfig).Generated with Claude Code