@@ -8,8 +8,12 @@ import (
88 "context"
99 "fmt"
1010 "log/slog"
11+ "os"
12+ "path/filepath"
1113
14+ "github.com/adrg/xdg"
1215 "github.com/docker/docker/client"
16+ "gopkg.in/yaml.v3"
1317
1418 "github.com/stacklok/toolhive/pkg/container/runtime"
1519)
@@ -49,15 +53,64 @@ const (
4953
5054var supportedSocketPaths = []runtime.Type {runtime .TypePodman , runtime .TypeDocker , runtime .TypeColima }
5155
56+ // socketPathConfig holds optional socket path overrides loaded from config.
57+ type socketPathConfig struct {
58+ podmanSocket string
59+ dockerSocket string
60+ colimaSocket string
61+ }
62+
63+ // containerRuntimeOverrides is a minimal config shape for reading socket path
64+ // overrides from the ToolHive config file. A full pkg/config import is not
65+ // possible here due to an import cycle through pkg/transport -> pkg/container.
66+ type containerRuntimeOverrides struct {
67+ ContainerRuntime struct {
68+ DockerSocket string `yaml:"docker_socket,omitempty"`
69+ PodmanSocket string `yaml:"podman_socket,omitempty"`
70+ ColimaSocket string `yaml:"colima_socket,omitempty"`
71+ } `yaml:"container_runtime,omitempty"`
72+ }
73+
74+ // loadSocketOverrides reads socket path overrides from the ToolHive config file.
75+ // Best-effort: returns empty overrides on any error so auto-detection takes over.
76+ func loadSocketOverrides () socketPathConfig {
77+ configPath , err := xdg .ConfigFile ("toolhive/config.yaml" )
78+ if err != nil {
79+ slog .Debug ("failed to resolve config path for socket overrides" , "error" , err )
80+ return socketPathConfig {}
81+ }
82+
83+ // #nosec G304: path is derived from XDG config dir, not user input.
84+ data , err := os .ReadFile (filepath .Clean (configPath ))
85+ if err != nil {
86+ slog .Debug ("failed to read config file for socket overrides" , "error" , err )
87+ return socketPathConfig {}
88+ }
89+
90+ var cfg containerRuntimeOverrides
91+ if err := yaml .Unmarshal (data , & cfg ); err != nil {
92+ slog .Debug ("failed to parse config file for socket overrides" , "error" , err )
93+ return socketPathConfig {}
94+ }
95+
96+ return socketPathConfig {
97+ dockerSocket : cfg .ContainerRuntime .DockerSocket ,
98+ podmanSocket : cfg .ContainerRuntime .PodmanSocket ,
99+ colimaSocket : cfg .ContainerRuntime .ColimaSocket ,
100+ }
101+ }
102+
52103// NewDockerClient creates a new container client
53104func NewDockerClient (ctx context.Context ) (* client.Client , string , runtime.Type , error ) {
54105 var lastErr error
55106
107+ overrides := loadSocketOverrides ()
108+
56109 // We try to find a container socket for the given runtime
57110 // We try Podman first, then Docker as fallback
58111 for _ , sp := range supportedSocketPaths {
59112 // Try to find a container socket for the given runtime
60- socketPath , runtimeType , err := findContainerSocket (sp )
113+ socketPath , runtimeType , err := findContainerSocket (sp , overrides )
61114 if err != nil {
62115 //nolint:gosec // G706: runtime type from internal config
63116 slog .Debug ("failed to find socket" , "runtime" , sp , "error" , err )
@@ -105,7 +158,7 @@ func newClientWithSocketPath(ctx context.Context, socketPath string) (*client.Cl
105158}
106159
107160// findContainerSocket finds a container socket path, preferring Podman over Docker
108- func findContainerSocket (rt runtime.Type ) (string , runtime.Type , error ) {
161+ func findContainerSocket (rt runtime.Type , overrides socketPathConfig ) (string , runtime.Type , error ) {
109162 // Use platform-specific implementation
110- return findPlatformContainerSocket (rt )
163+ return findPlatformContainerSocket (rt , overrides )
111164}
0 commit comments