Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Let a job start service containers beside it

c1929d2fceb5e9e65204d5eef907d0781b9a5391 · Keenan Nemetz · 5d ago · parent 1d06b008

ci/agent/sandbox_test.go +159−8

@@ -1,6 +1,10 @@
1 1 package agent
2 2
3 3 import (
4 "context"
5 "os"
6 "path/filepath"
7 "strconv"
4 8 "strings"
5 9 "testing"
6 10
@@ -26,25 +30,30 @@ func TestNetworkForUsesTheJobsRequest(t *testing.T) {
26 30 job string
27 31 want string
28 32 wantError string
33 services []runnerService
29 34 }{
30 35 // A job that says nothing gets the runner's default, which is what every
31 36 // workflow meant before the field existed.
32 {"silent job on a bridge runner", ci.NetworkBridge, "", ci.NetworkBridge, ""},
33 {"silent job on a closed runner", ci.NetworkNone, "", ci.NetworkNone, ""},
37 {"silent job on a bridge runner", ci.NetworkBridge, "", ci.NetworkBridge, "", nil},
38 {"silent job on a closed runner", ci.NetworkNone, "", ci.NetworkNone, "", nil},
34 39 // An explicit request wins in both directions: a bridge runner can withhold
35 40 // the network, and a closed runner never silently provides one.
36 {"none on a bridge runner", ci.NetworkBridge, ci.NetworkNone, ci.NetworkNone, ""},
37 {"none on a closed runner", ci.NetworkNone, ci.NetworkNone, ci.NetworkNone, ""},
38 {"bridge on a bridge runner", ci.NetworkBridge, ci.NetworkBridge, ci.NetworkBridge, ""},
39 {"bridge on a closed runner", ci.NetworkNone, ci.NetworkBridge, ci.NetworkNone, "network: bridge"},
41 {"none on a bridge runner", ci.NetworkBridge, ci.NetworkNone, ci.NetworkNone, "", nil},
42 {"none on a closed runner", ci.NetworkNone, ci.NetworkNone, ci.NetworkNone, "", nil},
43 {"bridge on a bridge runner", ci.NetworkBridge, ci.NetworkBridge, ci.NetworkBridge, "", nil},
44 {"bridge on a closed runner", ci.NetworkNone, ci.NetworkBridge, ci.NetworkNone, "network: bridge", nil},
40 45 // A value the validator would refuse cannot arrive through a push, but the
41 46 // runner checks the job it was handed rather than trusting the row.
42 {"unknown value", ci.NetworkBridge, "host", ci.NetworkBridge, "not a network mode"},
47 {"unknown value", ci.NetworkBridge, "host", ci.NetworkBridge, "not a network mode", nil},
48 // A service lives on the job's network, so the job has to ask for one.
49 {"services need bridge", ci.NetworkBridge, "", ci.NetworkBridge, "network: bridge", []runnerService{{ID: "db", Image: "postgres:17"}}},
50 {"services with none", ci.NetworkBridge, ci.NetworkNone, ci.NetworkNone, "network: bridge", []runnerService{{ID: "db", Image: "postgres:17"}}},
51 {"services with a bridge job", ci.NetworkBridge, ci.NetworkBridge, ci.NetworkBridge, "", []runnerService{{ID: "db", Image: "postgres:17"}}},
43 52 }
44 53 for _, c := range cases {
45 54 t.Run(c.name, func(t *testing.T) {
46 55 box := runnerBox{Config: container.Config{Network: c.runner}}
47 cfg, err := box.networkFor(&runnerJob{ID: "r1", Network: c.job})
56 cfg, err := box.networkFor(&runnerJob{ID: "r1", Network: c.job, Services: c.services})
48 57 if c.wantError != "" {
49 58 if err == nil {
50 59 t.Fatalf("network %q on a %s runner was accepted", c.job, c.runner)
@@ -63,3 +72,145 @@ func TestNetworkForUsesTheJobsRequest(t *testing.T) {
63 72 })
64 73 }
65 74 }
75
76 // The job sandbox stops the job first, reads a service's output, then removes the
77 // service and the network. The failure paths rely on this order: logs are gone once
78 // the container is removed, and a network with a member cannot be removed.
79 func TestJobSandboxCollectsServiceLogsBeforeStopping(t *testing.T) {
80 dir := t.TempDir()
81 state := filepath.Join(dir, "state")
82 if err := os.MkdirAll(state, 0o755); err != nil {
83 t.Fatal(err)
84 }
85 script := `#!/bin/sh
86 cmd="$1"; shift
87 state="${FAKE_STATE:?}"
88 case "$cmd" in
89 run)
90 name=
91 while [ $# -gt 0 ]; do
92 case "$1" in
93 --name) name="$2"; shift 2 ;;
94 *) shift ;;
95 esac
96 done
97 printf 'run %s\n' "$name" >> "$state/calls"
98 ;;
99 logs)
100 printf 'db says hi\n'
101 printf 'logs %s\n' "$1" >> "$state/calls"
102 ;;
103 rm)
104 for last in "$@"; do :; done
105 printf 'rm %s\n' "$last" >> "$state/calls"
106 ;;
107 network)
108 sub="$1"; shift
109 case "$sub" in
110 create)
111 name=
112 while [ $# -gt 0 ]; do
113 case "$1" in
114 --label) shift 2 ;;
115 *) name="$1"; shift ;;
116 esac
117 done
118 printf 'network create %s\n' "$name" >> "$state/calls"
119 ;;
120 rm)
121 printf 'network rm %s\n' "$1" >> "$state/calls"
122 ;;
123 esac
124 ;;
125 esac
126 `
127 if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil {
128 t.Fatal(err)
129 }
130 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
131 t.Setenv("FAKE_STATE", state)
132
133 cfg, err := containerOptions(Options{ContainerRuntime: "docker"}, t.TempDir())
134 if err != nil {
135 t.Fatal(err)
136 }
137 net, err := container.CreateNetwork(context.Background(), cfg.Config, "vole-test-net", map[string]string{"vole.runner.id": cfg.id})
138 if err != nil {
139 t.Fatal(err)
140 }
141 svc, err := container.StartService(cfg.Config, container.ServiceSpec{
142 Name: "svc-db", Alias: "db", Image: "postgres:17",
143 })
144 if err != nil {
145 t.Fatal(err)
146 }
147 box := &jobSandbox{services: []*container.Container{svc}, serviceIDs: []string{"db"}, network: net}
148 var out strings.Builder
149 box.StopWithLogs(&out)
150 if got := out.String(); !strings.Contains(got, "=== service db ===") || !strings.Contains(got, "db says hi") {
151 t.Fatalf("service log = %q", got)
152 }
153 calls, err := os.ReadFile(filepath.Join(state, "calls"))
154 if err != nil {
155 t.Fatal(err)
156 }
157 order := string(calls)
158 logsAt := strings.Index(order, "logs svc-db")
159 rmAt := strings.Index(order, "rm svc-db")
160 netAt := strings.Index(order, "network rm vole-test-net")
161 if logsAt < 0 || rmAt < 0 || netAt < 0 || !(logsAt < rmAt && rmAt < netAt) {
162 t.Fatalf("wrong cleanup order:\n%s", order)
163 }
164 // A second stop must not re-read or re-remove anything.
165 out.Reset()
166 box.StopWithLogs(&out)
167 if out.Len() != 0 {
168 t.Fatalf("a second stop wrote %q", out.String())
169 }
170 }
171
172 func TestCleanupStaleNetworksSkipsALiveRunner(t *testing.T) {
173 dir := t.TempDir()
174 state := filepath.Join(dir, "calls")
175 script := `#!/bin/sh
176 cmd="$1"; shift
177 case "$cmd" in
178 network)
179 sub="$1"; shift
180 case "$sub" in
181 ls)
182 printf 'dead-net\nlive-net\n'
183 ;;
184 inspect)
185 for last in "$@"; do :; done
186 case "$last" in
187 dead-net) printf '{"vole.runner.pid":"999999"}\n' ;;
188 live-net) printf '{"vole.runner.pid":"%s"}\n' "$FAKE_LIVE_PID" ;;
189 esac
190 ;;
191 rm)
192 printf 'network rm %s\n' "$1" >> "${FAKE_STATE:?}"
193 ;;
194 esac
195 ;;
196 esac
197 `
198 if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil {
199 t.Fatal(err)
200 }
201 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
202 t.Setenv("FAKE_STATE", state)
203 t.Setenv("FAKE_LIVE_PID", strconv.Itoa(os.Getpid()))
204
205 cleanupStaleNetworks(runnerBox{Config: container.Config{Runtime: "docker"}, id: "runner1"})
206 calls, err := os.ReadFile(state)
207 if err != nil {
208 t.Fatal(err)
209 }
210 if !strings.Contains(string(calls), "network rm dead-net") {
211 t.Fatalf("a dead runner's network was not removed:\n%s", calls)
212 }
213 if strings.Contains(string(calls), "network rm live-net") {
214 t.Fatalf("a live runner's network was removed:\n%s", calls)
215 }
216 }