Skip to content

Commit 4405213

Browse files
authored
Windows: show correct Docker Desktop start command (#162)
1 parent 59d5815 commit 4405213

3 files changed

Lines changed: 83 additions & 2 deletions

File tree

internal/runtime/docker.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"log"
99
"os"
10+
"os/exec"
1011
"path/filepath"
1112
stdruntime "runtime"
1213
"strconv"
@@ -83,21 +84,37 @@ func (d *DockerRuntime) EmitUnhealthyError(sink output.Sink, err error) {
8384
actions := []output.ErrorAction{
8485
{Label: "Install Docker:", Value: "https://docs.docker.com/get-docker/"},
8586
}
87+
summary := err.Error()
8688
switch stdruntime.GOOS {
8789
case "darwin":
8890
actions = append([]output.ErrorAction{{Label: "Start Docker Desktop:", Value: "open -a Docker"}}, actions...)
8991
case "linux":
9092
actions = append([]output.ErrorAction{{Label: "Start Docker:", Value: "sudo systemctl start docker"}}, actions...)
9193
case "windows":
92-
actions = append([]output.ErrorAction{{Label: "Start Docker Desktop:", Value: "Start-Process 'Docker Desktop'"}}, actions...)
94+
actions = append([]output.ErrorAction{{Label: "Start Docker Desktop:", Value: windowsDockerStartCommand(os.Getenv, exec.LookPath)}}, actions...)
95+
// Suppress the raw error: on Windows it's a named-pipe message that users can't act on.
96+
summary = ""
9397
}
9498
output.EmitError(sink, output.ErrorEvent{
9599
Title: "Docker is not available",
96-
Summary: err.Error(),
100+
Summary: summary,
97101
Actions: actions,
98102
})
99103
}
100104

105+
// PSModulePath is always set by PowerShell and never by cmd.exe; use it to pick the right start command.
106+
// Prefers "docker desktop start" (documented CLI method); falls back to the full executable path.
107+
func windowsDockerStartCommand(getenv func(string) string, lookPath func(string) (string, error)) string {
108+
if _, err := lookPath("docker"); err == nil {
109+
return "docker desktop start"
110+
}
111+
const exePath = `C:\Program Files\Docker\Docker\Docker Desktop.exe`
112+
if getenv("PSModulePath") != "" {
113+
return "& '" + exePath + "'"
114+
}
115+
return `"` + exePath + `"`
116+
}
117+
101118
func (d *DockerRuntime) PullImage(ctx context.Context, imageName string, progress chan<- PullProgress) error {
102119
reader, err := d.client.ImagePull(ctx, imageName, image.PullOptions{})
103120
if err != nil {

internal/runtime/docker_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package runtime
22

33
import (
4+
"errors"
45
"os"
56
"path/filepath"
67
"testing"
@@ -54,3 +55,24 @@ func TestSocketPath_ReturnsEmptyForTCPHost(t *testing.T) {
5455

5556
assert.Equal(t, "", rt.SocketPath())
5657
}
58+
59+
func TestWindowsDockerStartCommand_DockerAvailable(t *testing.T) {
60+
lookPath := func(string) (string, error) { return "/usr/bin/docker", nil }
61+
assert.Equal(t, "docker desktop start", windowsDockerStartCommand(func(string) string { return "" }, lookPath))
62+
}
63+
64+
func TestWindowsDockerStartCommand_PowerShellFallback(t *testing.T) {
65+
lookPath := func(string) (string, error) { return "", errors.New("not found") }
66+
getenv := func(key string) string {
67+
if key == "PSModulePath" {
68+
return `C:\Windows\System32\WindowsPowerShell\v1.0\Modules`
69+
}
70+
return ""
71+
}
72+
assert.Equal(t, `& 'C:\Program Files\Docker\Docker\Docker Desktop.exe'`, windowsDockerStartCommand(getenv, lookPath))
73+
}
74+
75+
func TestWindowsDockerStartCommand_CmdFallback(t *testing.T) {
76+
lookPath := func(string) (string, error) { return "", errors.New("not found") }
77+
assert.Equal(t, `"C:\Program Files\Docker\Docker\Docker Desktop.exe"`, windowsDockerStartCommand(func(string) string { return "" }, lookPath))
78+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package integration_test
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
7+
"github.com/localstack/lstk/test/integration/env"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// windowsDockerErrorEnv returns an Environ with an invalid DOCKER_HOST and no PSModulePath.
13+
func windowsDockerErrorEnv() env.Environ {
14+
return env.Without(env.Key("PSModulePath"), env.Key("DOCKER_HOST")).
15+
With(env.AuthToken, "fake-token").
16+
With(env.Key("DOCKER_HOST"), "tcp://localhost:1")
17+
}
18+
19+
// Verifies that when docker is in PATH, lstk suggests "docker desktop start".
20+
func TestWindowsDockerErrorShowsDockerCLICommand(t *testing.T) {
21+
if runtime.GOOS != "windows" {
22+
t.Skip("Windows-only test")
23+
}
24+
25+
stdout, _, err := runLstk(t, testContext(t), "", windowsDockerErrorEnv(), "start")
26+
require.Error(t, err)
27+
requireExitCode(t, 1, err)
28+
assert.Contains(t, stdout, "docker desktop start")
29+
}
30+
31+
// Verifies that the verbose Docker error message is suppressed on Windows.
32+
func TestWindowsDockerErrorOmitsVerboseSummary(t *testing.T) {
33+
if runtime.GOOS != "windows" {
34+
t.Skip("Windows-only test")
35+
}
36+
37+
stdout, _, err := runLstk(t, testContext(t), "", windowsDockerErrorEnv(), "start")
38+
require.Error(t, err)
39+
requireExitCode(t, 1, err)
40+
assert.Contains(t, stdout, "Docker is not available")
41+
assert.NotContains(t, stdout, "cannot connect to Docker daemon")
42+
}

0 commit comments

Comments
 (0)