diff --git a/internal/runtime/docker.go b/internal/runtime/docker.go index 40360f2..b3408c5 100644 --- a/internal/runtime/docker.go +++ b/internal/runtime/docker.go @@ -64,9 +64,40 @@ func probeSocket(candidates ...string) string { return "" } +// isVM reports whether the Docker daemon is running inside a VM (e.g., Colima, OrbStack). +// In these cases the socket is remapped inside the VM and the container sees it at +// /var/run/docker.sock even if the CLI connects via a user-scoped socket path. +func (d *DockerRuntime) isVM() bool { + host := d.client.DaemonHost() + if strings.HasPrefix(host, "unix://") { + socketPath := strings.TrimPrefix(host, "unix://") + // Check for known VM-based Docker socket locations + home, _ := os.UserHomeDir() + vmSockets := []string{ + filepath.Join(home, ".colima", "default", "docker.sock"), + filepath.Join(home, ".colima", "docker.sock"), + filepath.Join(home, ".orbstack", "run", "docker.sock"), + } + for _, vmSock := range vmSockets { + if socketPath == vmSock { + return true + } + } + } + return false +} + +// SocketPath returns the daemon-visible Unix socket path to bind-mount into +// containers so LocalStack can launch nested workloads such as Lambda functions. +// For VM-based Docker (Colima, OrbStack) returns /var/run/docker.sock as the +// socket is remapped inside the VM. For rootless or custom setups, returns the +// actual socket path extracted from the daemon host. func (d *DockerRuntime) SocketPath() string { host := d.client.DaemonHost() if strings.HasPrefix(host, "unix://") { + if d.isVM() { + return "/var/run/docker.sock" + } return strings.TrimPrefix(host, "unix://") } return "" diff --git a/internal/runtime/docker_test.go b/internal/runtime/docker_test.go index f76ec5e..31a1f17 100644 --- a/internal/runtime/docker_test.go +++ b/internal/runtime/docker_test.go @@ -41,11 +41,26 @@ func TestProbeSocket_ReturnsEmptyForNoCandidates(t *testing.T) { } func TestSocketPath_ExtractsUnixPath(t *testing.T) { - cli, err := client.NewClientWithOpts(client.WithHost("unix:///home/user/.colima/default/docker.sock")) - require.NoError(t, err) - rt := &DockerRuntime{client: cli} - - assert.Equal(t, "/home/user/.colima/default/docker.sock", rt.SocketPath()) + t.Run("standard socket returns daemon path", func(t *testing.T) { + cli, err := client.NewClientWithOpts(client.WithHost("unix:///var/run/docker.sock")) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + }) + + t.Run("non-standard socket returns daemon path", func(t *testing.T) { + cli, err := client.NewClientWithOpts(client.WithHost("unix:///home/user/.colima/default/docker.sock")) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/home/user/.colima/default/docker.sock", rt.SocketPath()) + }) + + t.Run("orbstack socket returns daemon path", func(t *testing.T) { + cli, err := client.NewClientWithOpts(client.WithHost("unix:///Users/user/.orbstack/run/docker.sock")) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/Users/user/.orbstack/run/docker.sock", rt.SocketPath()) + }) } func TestSocketPath_ReturnsEmptyForTCPHost(t *testing.T) { @@ -56,6 +71,57 @@ func TestSocketPath_ReturnsEmptyForTCPHost(t *testing.T) { assert.Equal(t, "", rt.SocketPath()) } +func TestSocketPath_VMDetection(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + + t.Run("colima socket exists returns remapped path", func(t *testing.T) { + colimaSock := filepath.Join(home, ".colima", "default", "docker.sock") + require.NoError(t, os.MkdirAll(filepath.Dir(colimaSock), 0o755)) + f, err := os.Create(colimaSock) + require.NoError(t, err) + require.NoError(t, f.Close()) + t.Cleanup(func() { + require.NoError(t, os.Remove(colimaSock)) + }) + + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + colimaSock)) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + }) + + t.Run("orbstack socket exists returns remapped path", func(t *testing.T) { + orbstackSock := filepath.Join(home, ".orbstack", "run", "docker.sock") + require.NoError(t, os.MkdirAll(filepath.Dir(orbstackSock), 0o755)) + f, err := os.Create(orbstackSock) + require.NoError(t, err) + require.NoError(t, f.Close()) + t.Cleanup(func() { + require.NoError(t, os.Remove(orbstackSock)) + }) + + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + orbstackSock)) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, "/var/run/docker.sock", rt.SocketPath()) + }) + + t.Run("rootless socket exists returns actual path", func(t *testing.T) { + // Use a non-VM socket path (short path to avoid Docker client limit) + rootlessSock := "/tmp/lstk-docker.sock" + require.NoError(t, os.WriteFile(rootlessSock, nil, 0o600)) + t.Cleanup(func() { + require.NoError(t, os.Remove(rootlessSock)) + }) + + cli, err := client.NewClientWithOpts(client.WithHost("unix://" + rootlessSock)) + require.NoError(t, err) + rt := &DockerRuntime{client: cli} + assert.Equal(t, rootlessSock, rt.SocketPath()) + }) +} + func TestWindowsDockerStartCommand_DockerAvailable(t *testing.T) { lookPath := func(string) (string, error) { return "/usr/bin/docker", nil } assert.Equal(t, "docker desktop start", windowsDockerStartCommand(func(string) string { return "" }, lookPath)) diff --git a/test/integration/start_test.go b/test/integration/start_test.go index 008f27d..da9cdb9 100644 --- a/test/integration/start_test.go +++ b/test/integration/start_test.go @@ -172,6 +172,8 @@ func TestStartCommandSetsUpContainerCorrectly(t *testing.T) { assert.True(t, hasBindTarget(inspect.HostConfig.Binds, "/var/run/docker.sock"), "expected Docker socket bind mount to /var/run/docker.sock, got: %v", inspect.HostConfig.Binds) + assert.True(t, hasBindSource(inspect.HostConfig.Binds, "/var/run/docker.sock"), + "expected Docker socket bind mount from /var/run/docker.sock, got: %v", inspect.HostConfig.Binds) envVars := containerEnvToMap(inspect.Config.Env) assert.Equal(t, "unix:///var/run/docker.sock", envVars["DOCKER_HOST"]) @@ -247,6 +249,16 @@ func hasBindTarget(binds []string, containerPath string) bool { return false } +func hasBindSource(binds []string, hostPath string) bool { + for _, b := range binds { + parts := strings.Split(b, ":") + if len(parts) >= 2 && parts[0] == hostPath { + return true + } + } + return false +} + func cleanup() { ctx := context.Background() _ = dockerClient.ContainerStop(ctx, containerName, container.StopOptions{})