From 21f7bbd81c5cc2ecf63579e4f14cfd54cc68fcf8 Mon Sep 17 00:00:00 2001 From: ish-g09 Date: Tue, 22 Sep 2026 19:37:25 +0530 Subject: [PATCH] fix: honor active docker context on macOS when default socket unavailable Signed-off-by: ish-g09 --- cmd/testDryRun.go | 4 +- pkg/connectors/container_client.go | 43 ++++++ pkg/connectors/container_client_test.go | 178 ++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 pkg/connectors/container_client_test.go diff --git a/cmd/testDryRun.go b/cmd/testDryRun.go index b8ebf7d0..d681f70f 100644 --- a/cmd/testDryRun.go +++ b/cmd/testDryRun.go @@ -60,12 +60,12 @@ func configureDriver(driver string) error { case "podman": return setupPodman() case "docker": - return nil // testcontainers-go's default, via DOCKER_HOST + return connectors.ConfigureDockerHost() // ensure DOCKER_HOST is set before testcontainers-go connects case "": if shouldUsePodman() { return setupPodman() } - return nil + return connectors.ConfigureDockerHost() default: return errors.Wrapf(errors.KindUsage, "unsupported --driver %q (use 'docker' or 'podman')", driver) } diff --git a/pkg/connectors/container_client.go b/pkg/connectors/container_client.go index e231649f..53588e5e 100644 --- a/pkg/connectors/container_client.go +++ b/pkg/connectors/container_client.go @@ -22,6 +22,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "runtime" "strings" "time" @@ -74,7 +75,46 @@ func NewContainerClient(driver string) (ContainerClient, error) { } } +var ( + currentGOOS = runtime.GOOS + execCommand = exec.Command + defaultSocketPath = "/var/run/docker.sock" +) + +// ConfigureDockerHost ensures DOCKER_HOST points to an active Docker daemon socket. +// On macOS, Docker Desktop may not install the privileged /var/run/docker.sock symlink. +// If DOCKER_HOST is not explicitly set, we look for the user socket or inspect the active context. +func ConfigureDockerHost() error { + if os.Getenv("DOCKER_HOST") != "" || currentGOOS != "darwin" { + return nil + } + + if _, err := os.Stat(defaultSocketPath); err == nil { + return nil + } + + // Query the endpoint from the active docker context first to honor user configuration. + out, err := execCommand("docker", "context", "inspect", "--format", "{{.Endpoints.docker.Host}}").Output() + if err == nil { + if endpoint := strings.TrimSpace(string(out)); endpoint != "" { + return os.Setenv("DOCKER_HOST", endpoint) + } + } + + // Fall back to the standard Docker Desktop socket path on macOS. + if homeDir, err := os.UserHomeDir(); err == nil { + userSocket := filepath.Join(homeDir, ".docker", "run", "docker.sock") + if _, err := os.Stat(userSocket); err == nil { + return os.Setenv("DOCKER_HOST", "unix://"+userSocket) + } + } + + return nil +} func NewDockerClient() (*containerClient, error) { + if err := ConfigureDockerHost(); err != nil { + return nil, err + } cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { @@ -89,6 +129,9 @@ func NewDockerClient() (*containerClient, error) { // an unreachable endpoint (e.g. a stopped Podman machine) before testcontainers-go // silently resolves to a different runtime. func PingDockerHost() error { + if err := ConfigureDockerHost(); err != nil { + return err + } cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return err diff --git a/pkg/connectors/container_client_test.go b/pkg/connectors/container_client_test.go new file mode 100644 index 00000000..d0435b12 --- /dev/null +++ b/pkg/connectors/container_client_test.go @@ -0,0 +1,178 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package connectors + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestHelperProcess intercepts execCommand calls during unit tests. +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + defer os.Exit(0) + + args := os.Args + for len(args) > 0 { + if args[0] == "--" { + args = args[1:] + break + } + args = args[1:] + } + if len(args) == 0 { + fmt.Fprintln(os.Stderr, "no command provided") + os.Exit(2) + } + + cmd, cmdArgs := args[0], args[1:] + switch cmd { + case "docker": + if os.Getenv("HELPER_DOCKER_FAIL") == "1" { + os.Exit(1) + } + if len(cmdArgs) >= 4 && cmdArgs[0] == "context" && cmdArgs[1] == "inspect" { + fmt.Print("unix:///mock/active/docker.sock\n") + } + default: + os.Exit(2) + } +} + +func mockExecCommand(envVars ...string) func(string, ...string) *exec.Cmd { + return func(name string, args ...string) *exec.Cmd { + cs := []string{"-test.run=TestHelperProcess", "--", name} + cs = append(cs, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1") + cmd.Env = append(cmd.Env, envVars...) + return cmd + } +} + +func TestConfigureDockerHost_PreservesExistingDockerHost(t *testing.T) { + t.Setenv("DOCKER_HOST", "unix:///existing/custom.sock") + + err := ConfigureDockerHost() + require.NoError(t, err) + assert.Equal(t, "unix:///existing/custom.sock", os.Getenv("DOCKER_HOST")) +} + +func TestConfigureDockerHost_NonDarwin(t *testing.T) { + t.Setenv("DOCKER_HOST", "") + origGOOS := currentGOOS + currentGOOS = "linux" + defer func() { currentGOOS = origGOOS }() + + err := ConfigureDockerHost() + require.NoError(t, err) + assert.Empty(t, os.Getenv("DOCKER_HOST")) +} + +func TestConfigureDockerHost_DefaultSocketExists(t *testing.T) { + t.Setenv("DOCKER_HOST", "") + origGOOS := currentGOOS + origDefaultPath := defaultSocketPath + currentGOOS = "darwin" + tmpDir := t.TempDir() + dummySock := filepath.Join(tmpDir, "docker.sock") + require.NoError(t, os.WriteFile(dummySock, []byte(""), 0o600)) + defaultSocketPath = dummySock + defer func() { + currentGOOS = origGOOS + defaultSocketPath = origDefaultPath + }() + + err := ConfigureDockerHost() + require.NoError(t, err) + assert.Empty(t, os.Getenv("DOCKER_HOST")) +} + +func TestConfigureDockerHost_InspectContextSuccess(t *testing.T) { + t.Setenv("DOCKER_HOST", "") + origGOOS := currentGOOS + origDefaultPath := defaultSocketPath + origExec := execCommand + currentGOOS = "darwin" + defaultSocketPath = filepath.Join(t.TempDir(), "nonexistent.sock") + execCommand = mockExecCommand() + defer func() { + currentGOOS = origGOOS + defaultSocketPath = origDefaultPath + execCommand = origExec + }() + + err := ConfigureDockerHost() + require.NoError(t, err) + assert.Equal(t, "unix:///mock/active/docker.sock", os.Getenv("DOCKER_HOST")) +} + +func TestConfigureDockerHost_FallbackToUserSocket(t *testing.T) { + t.Setenv("DOCKER_HOST", "") + origGOOS := currentGOOS + origDefaultPath := defaultSocketPath + origExec := execCommand + currentGOOS = "darwin" + defaultSocketPath = filepath.Join(t.TempDir(), "nonexistent.sock") + execCommand = mockExecCommand("HELPER_DOCKER_FAIL=1") + + tempHome := t.TempDir() + userSockDir := filepath.Join(tempHome, ".docker", "run") + require.NoError(t, os.MkdirAll(userSockDir, 0o755)) + userSockFile := filepath.Join(userSockDir, "docker.sock") + require.NoError(t, os.WriteFile(userSockFile, []byte(""), 0o600)) + t.Setenv("HOME", tempHome) + + defer func() { + currentGOOS = origGOOS + defaultSocketPath = origDefaultPath + execCommand = origExec + }() + + err := ConfigureDockerHost() + require.NoError(t, err) + assert.Equal(t, "unix://"+userSockFile, os.Getenv("DOCKER_HOST")) +} + +func TestConfigureDockerHost_NeitherContextNorUserSocketAvailable(t *testing.T) { + t.Setenv("DOCKER_HOST", "") + origGOOS := currentGOOS + origDefaultPath := defaultSocketPath + origExec := execCommand + currentGOOS = "darwin" + defaultSocketPath = filepath.Join(t.TempDir(), "nonexistent.sock") + execCommand = mockExecCommand("HELPER_DOCKER_FAIL=1") + t.Setenv("HOME", t.TempDir()) + + defer func() { + currentGOOS = origGOOS + defaultSocketPath = origDefaultPath + execCommand = origExec + }() + + err := ConfigureDockerHost() + require.NoError(t, err) + assert.Empty(t, os.Getenv("DOCKER_HOST")) +}