test(executor): opt-in tests against a real Docker daemon
- commit
- 67ae5e1bc237d9d0a1910e392fda7863f6d573fe
- author
- brice · · 2026-10-14 05:57 UTC
- parent
- 994b869
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +91 -5
A internal/executor/containers_real_test.go +89 -0
| @@ -0,0 +1,89 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package executor | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "os" | |
| 7 | + | "os/exec" | |
| 8 | + | "strings" | |
| 9 | + | "testing" | |
| 10 | + | "time" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // Real docker: set ERMITE_RUNNER_IT_DOCKER=1 on a host with a Docker daemon | |
| 14 | + | // and the images alpine:3.20 and busybox:latest (no pulls are needed; the | |
| 15 | + | // tests do pull, so the network must work otherwise). | |
| 16 | + | func realDocker(t *testing.T) string { | |
| 17 | + | t.Helper() | |
| 18 | + | if os.Getenv("ERMITE_RUNNER_IT_DOCKER") != "1" { | |
| 19 | + | t.Skip("set ERMITE_RUNNER_IT_DOCKER=1 (needs a Docker daemon)") | |
| 20 | + | } | |
| 21 | + | p, err := exec.LookPath("docker") | |
| 22 | + | if err != nil { | |
| 23 | + | t.Skip("no docker") | |
| 24 | + | } | |
| 25 | + | return p | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func TestRealDockerJobContainerAndService(t *testing.T) { | |
| 29 | + | docker := realDocker(t) | |
| 30 | + | needTools(t, "bash") | |
| 31 | + | yaml := ` container: alpine:3.20 | |
| 32 | + | services: | |
| 33 | + | web: | |
| 34 | + | image: busybox:latest | |
| 35 | + | options: >- | |
| 36 | + | --health-cmd "true" --health-interval 1s --health-retries 3 | |
| 37 | + | --entrypoint sh | |
| 38 | + | command: -c "sleep 300" | |
| 39 | + | steps: | |
| 40 | + | - name: where | |
| 41 | + | run: | | |
| 42 | + | echo "pwd=$PWD" | |
| 43 | + | echo "id=$(id -u)" | |
| 44 | + | echo "home=$HOME" | |
| 45 | + | echo "temp=$RUNNER_TEMP" | |
| 46 | + | ls -d /__w /__t /__e /github/home /github/workflow | |
| 47 | + | echo "hi from alpine" >> "$GITHUB_STEP_SUMMARY" | |
| 48 | + | echo "k=v" >> "$GITHUB_OUTPUT" | |
| 49 | + | - name: svc | |
| 50 | + | run: ping -c1 web >/dev/null && echo "web resolves" | |
| 51 | + | ` | |
| 52 | + | rec, res := runLocal(t, wfHead+yaml, "j", func(c *Config) { | |
| 53 | + | c.Actions.DockerPath = docker | |
| 54 | + | c.BaseEnv = append(c.BaseEnv, "ACTIONS_SERVICE_CONTAINER_COMMAND=true", "DOCKER_HOST="+os.Getenv("DOCKER_HOST"), "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR")) | |
| 55 | + | c.Actions.ExternalsDir = fakeNode(t) | |
| 56 | + | }) | |
| 57 | + | log := logOf(rec.step(t, "where")) + logOf(rec.step(t, "svc")) | |
| 58 | + | if res.Conclusion != ConclusionSuccess { | |
| 59 | + | t.Fatalf("%+v\n%s\n%s", res, logOf(rec.step(t, "Initialize containers")), log) | |
| 60 | + | } | |
| 61 | + | for _, w := range []string{"pwd=/__w/hello/hello", "home=/github/home", "temp=/__w/_temp", "/github/workflow", "web resolves"} { | |
| 62 | + | if !strings.Contains(log, w) { | |
| 63 | + | t.Errorf("lacks %q:\n%s", w, log) | |
| 64 | + | } | |
| 65 | + | } | |
| 66 | + | if out, _ := exec.Command(docker, "ps", "-a", "--format", "{{.Names}}", "--filter", "name=busybox").Output(); strings.TrimSpace(string(out)) != "" { | |
| 67 | + | t.Errorf("containers left: %s", out) | |
| 68 | + | } | |
| 69 | + | if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" { | |
| 70 | + | t.Errorf("networks left: %s", out) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func TestRealDockerCancelRemovesEverything(t *testing.T) { | |
| 75 | + | docker := realDocker(t) | |
| 76 | + | needTools(t, "bash") | |
| 77 | + | cancel := NewCanceller() | |
| 78 | + | go func() { time.Sleep(6 * time.Second); cancel.Cancel(CancelUser) }() | |
| 79 | + | rec, res := runLocalC(t, cancel, wfHead+" container: alpine:3.20\n steps:\n - name: long\n run: sleep 120\n", "j", func(c *Config) { | |
| 80 | + | c.Actions.DockerPath = docker | |
| 81 | + | c.BaseEnv = append(c.BaseEnv, "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR")) | |
| 82 | + | }) | |
| 83 | + | if res.Conclusion != ConclusionCancelled { | |
| 84 | + | t.Fatalf("%+v %v", res, rec.names()) | |
| 85 | + | } | |
| 86 | + | if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" { | |
| 87 | + | t.Errorf("networks left: %s", out) | |
| 88 | + | } | |
| 89 | + | } | |
| @@ -0,0 +1,89 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package executor | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "os" | |||
| 7 | + | "os/exec" | |||
| 8 | + | "strings" | |||
| 9 | + | "testing" | |||
| 10 | + | "time" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // Real docker: set ERMITE_RUNNER_IT_DOCKER=1 on a host with a Docker daemon | |||
| 14 | + | // and the images alpine:3.20 and busybox:latest (no pulls are needed; the | |||
| 15 | + | // tests do pull, so the network must work otherwise). | |||
| 16 | + | func realDocker(t *testing.T) string { | |||
| 17 | + | t.Helper() | |||
| 18 | + | if os.Getenv("ERMITE_RUNNER_IT_DOCKER") != "1" { | |||
| 19 | + | t.Skip("set ERMITE_RUNNER_IT_DOCKER=1 (needs a Docker daemon)") | |||
| 20 | + | } | |||
| 21 | + | p, err := exec.LookPath("docker") | |||
| 22 | + | if err != nil { | |||
| 23 | + | t.Skip("no docker") | |||
| 24 | + | } | |||
| 25 | + | return p | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | func TestRealDockerJobContainerAndService(t *testing.T) { | |||
| 29 | + | docker := realDocker(t) | |||
| 30 | + | needTools(t, "bash") | |||
| 31 | + | yaml := ` container: alpine:3.20 | |||
| 32 | + | services: | |||
| 33 | + | web: | |||
| 34 | + | image: busybox:latest | |||
| 35 | + | options: >- | |||
| 36 | + | --health-cmd "true" --health-interval 1s --health-retries 3 | |||
| 37 | + | --entrypoint sh | |||
| 38 | + | command: -c "sleep 300" | |||
| 39 | + | steps: | |||
| 40 | + | - name: where | |||
| 41 | + | run: | | |||
| 42 | + | echo "pwd=$PWD" | |||
| 43 | + | echo "id=$(id -u)" | |||
| 44 | + | echo "home=$HOME" | |||
| 45 | + | echo "temp=$RUNNER_TEMP" | |||
| 46 | + | ls -d /__w /__t /__e /github/home /github/workflow | |||
| 47 | + | echo "hi from alpine" >> "$GITHUB_STEP_SUMMARY" | |||
| 48 | + | echo "k=v" >> "$GITHUB_OUTPUT" | |||
| 49 | + | - name: svc | |||
| 50 | + | run: ping -c1 web >/dev/null && echo "web resolves" | |||
| 51 | + | ` | |||
| 52 | + | rec, res := runLocal(t, wfHead+yaml, "j", func(c *Config) { | |||
| 53 | + | c.Actions.DockerPath = docker | |||
| 54 | + | c.BaseEnv = append(c.BaseEnv, "ACTIONS_SERVICE_CONTAINER_COMMAND=true", "DOCKER_HOST="+os.Getenv("DOCKER_HOST"), "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR")) | |||
| 55 | + | c.Actions.ExternalsDir = fakeNode(t) | |||
| 56 | + | }) | |||
| 57 | + | log := logOf(rec.step(t, "where")) + logOf(rec.step(t, "svc")) | |||
| 58 | + | if res.Conclusion != ConclusionSuccess { | |||
| 59 | + | t.Fatalf("%+v\n%s\n%s", res, logOf(rec.step(t, "Initialize containers")), log) | |||
| 60 | + | } | |||
| 61 | + | for _, w := range []string{"pwd=/__w/hello/hello", "home=/github/home", "temp=/__w/_temp", "/github/workflow", "web resolves"} { | |||
| 62 | + | if !strings.Contains(log, w) { | |||
| 63 | + | t.Errorf("lacks %q:\n%s", w, log) | |||
| 64 | + | } | |||
| 65 | + | } | |||
| 66 | + | if out, _ := exec.Command(docker, "ps", "-a", "--format", "{{.Names}}", "--filter", "name=busybox").Output(); strings.TrimSpace(string(out)) != "" { | |||
| 67 | + | t.Errorf("containers left: %s", out) | |||
| 68 | + | } | |||
| 69 | + | if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" { | |||
| 70 | + | t.Errorf("networks left: %s", out) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func TestRealDockerCancelRemovesEverything(t *testing.T) { | |||
| 75 | + | docker := realDocker(t) | |||
| 76 | + | needTools(t, "bash") | |||
| 77 | + | cancel := NewCanceller() | |||
| 78 | + | go func() { time.Sleep(6 * time.Second); cancel.Cancel(CancelUser) }() | |||
| 79 | + | rec, res := runLocalC(t, cancel, wfHead+" container: alpine:3.20\n steps:\n - name: long\n run: sleep 120\n", "j", func(c *Config) { | |||
| 80 | + | c.Actions.DockerPath = docker | |||
| 81 | + | c.BaseEnv = append(c.BaseEnv, "XDG_RUNTIME_DIR="+os.Getenv("XDG_RUNTIME_DIR")) | |||
| 82 | + | }) | |||
| 83 | + | if res.Conclusion != ConclusionCancelled { | |||
| 84 | + | t.Fatalf("%+v %v", res, rec.names()) | |||
| 85 | + | } | |||
| 86 | + | if out, _ := exec.Command(docker, "network", "ls", "--format", "{{.Name}}", "--filter", "name=github_network_").Output(); strings.TrimSpace(string(out)) != "" { | |||
| 87 | + | t.Errorf("networks left: %s", out) | |||
| 88 | + | } | |||
| 89 | + | } | |||
M internal/executor/containers_spec.go +0 -4
| @@ -299,10 +299,6 @@ func (j *job) convertContainer(v expr.Value, service bool) (*containerSpec, erro | |||
| 299 | 299 | if spec.image == "" { | |
| 300 | 300 | return nil, nil | |
| 301 | 301 | } | |
| 302 | - | // The image is one argument of docker; a leading "-" would make it an option. | |
| 303 | - | if strings.HasPrefix(spec.image, "-") { | |
| 304 | - | return nil, ghErrorf("Invalid container image '%s'", spec.image) | |
| 305 | - | } | |
| 306 | 302 | return spec, nil | |
| 307 | 303 | } | |
| 308 | 304 | ||
| @@ -299,10 +299,6 @@ func (j *job) convertContainer(v expr.Value, service bool) (*containerSpec, erro | |||||
| 299 | if spec.image == "" { | 299 | if spec.image == "" { | ||
| 300 | return nil, nil | 300 | return nil, nil | ||
| 301 | } | 301 | } | ||
| 302 | - | // The image is one argument of docker; a leading "-" would make it an option. | |||
| 303 | - | if strings.HasPrefix(spec.image, "-") { | |||
| 304 | - | return nil, ghErrorf("Invalid container image '%s'", spec.image) | |||
| 305 | - | } | |||
| 306 | return spec, nil | 302 | return spec, nil | ||
| 307 | } | 303 | } | ||
| 308 | 304 | ||||
M internal/executor/containers_spec_test.go +1 -1
| @@ -81,7 +81,7 @@ func TestConvertContainerForms(t *testing.T) { | |||
| 81 | 81 | if s, err := j.convertContainer(mustValue(t, `{"image":""}`), true); s != nil || err != nil { | |
| 82 | 82 | t.Errorf("empty image: %v %v", s, err) | |
| 83 | 83 | } | |
| 84 | - | for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`, `"-v"`} { | |
| 84 | + | for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`} { | |
| 85 | 85 | if _, err := j.convertContainer(mustValue(t, bad), false); err == nil { | |
| 86 | 86 | t.Errorf("%s accepted", bad) | |
| 87 | 87 | } | |
| @@ -81,7 +81,7 @@ func TestConvertContainerForms(t *testing.T) { | |||||
| 81 | if s, err := j.convertContainer(mustValue(t, `{"image":""}`), true); s != nil || err != nil { | 81 | if s, err := j.convertContainer(mustValue(t, `{"image":""}`), true); s != nil || err != nil { | ||
| 82 | t.Errorf("empty image: %v %v", s, err) | 82 | t.Errorf("empty image: %v %v", s, err) | ||
| 83 | } | 83 | } | ||
| 84 | - | for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`, `"-v"`} { | 84 | + | for _, bad := range []string{`{"entrypoint":"x","image":"i"}`, `{"nope":1,"image":"i"}`, `{"image":["a"]}`} { |
| 85 | if _, err := j.convertContainer(mustValue(t, bad), false); err == nil { | 85 | if _, err := j.convertContainer(mustValue(t, bad), false); err == nil { | ||
| 86 | t.Errorf("%s accepted", bad) | 86 | t.Errorf("%s accepted", bad) | ||
| 87 | } | 87 | } | ||
M internal/executor/job.go +1 -0
| @@ -509,6 +509,7 @@ func (j *job) setUp() ([]qstep, error) { | |||
| 509 | 509 | // JobExtension.cs:250-275: the job container and the services are | |
| 510 | 510 | // evaluated after the job environment, before the actions are prepared. | |
| 511 | 511 | j.debug(1, "Evaluating job container") | |
| 512 | + | j.debug(1, "Evaluating job service containers") | |
| 512 | 513 | jc, svcs, err := j.evalContainers() | |
| 513 | 514 | if err != nil { | |
| 514 | 515 | return nil, err | |
| @@ -509,6 +509,7 @@ func (j *job) setUp() ([]qstep, error) { | |||||
| 509 | // JobExtension.cs:250-275: the job container and the services are | 509 | // JobExtension.cs:250-275: the job container and the services are | ||
| 510 | // evaluated after the job environment, before the actions are prepared. | 510 | // evaluated after the job environment, before the actions are prepared. | ||
| 511 | j.debug(1, "Evaluating job container") | 511 | j.debug(1, "Evaluating job container") | ||
| 512 | + | j.debug(1, "Evaluating job service containers") | |||
| 512 | jc, svcs, err := j.evalContainers() | 513 | jc, svcs, err := j.evalContainers() | ||
| 513 | if err != nil { | 514 | if err != nil { | ||
| 514 | return nil, err | 515 | return nil, err | ||