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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 8.4 KiB
1package agent
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9 "testing"
10
11 "rodent.software/vole/ci"
12 "rodent.software/vole/ci/container"
13)
14
15func TestContainerOptionsRequireRuntime(t *testing.T) {
16 if _, err := containerOptions(Options{ContainerRuntime: "definitely-not-a-runtime"}, "/tmp/cache"); err == nil {
17 t.Fatal("an unknown container runtime was accepted")
18 }
19}
20
21// The job's `network:` decides what the container gets, and the runner's own flag is
22// only the default for a job that says nothing. A job that asks for more than the
23// machine can give is refused before anything runs, rather than starting a build that
24// cannot reach what it needs.
25func TestNetworkForUsesTheJobsRequest(t *testing.T) {
26 t.Parallel()
27 cases := []struct {
28 name string
29 runner string
30 job string
31 want string
32 wantError string
33 services []runnerService
34 }{
35 // A job that says nothing gets the runner's default, which is what every
36 // workflow meant before the field existed.
37 {"silent job on a bridge runner", ci.NetworkBridge, "", ci.NetworkBridge, "", nil},
38 {"silent job on a closed runner", ci.NetworkNone, "", ci.NetworkNone, "", nil},
39 // An explicit request wins in both directions: a bridge runner can withhold
40 // the network, and a closed runner never silently provides one.
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},
45 // A value the validator would refuse cannot arrive through a push, but the
46 // runner checks the job it was handed rather than trusting the row.
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"}}},
52 }
53 for _, c := range cases {
54 t.Run(c.name, func(t *testing.T) {
55 box := runnerBox{Config: container.Config{Network: c.runner}}
56 cfg, err := box.networkFor(&runnerJob{ID: "r1", Network: c.job, Services: c.services})
57 if c.wantError != "" {
58 if err == nil {
59 t.Fatalf("network %q on a %s runner was accepted", c.job, c.runner)
60 }
61 if !strings.Contains(err.Error(), c.wantError) {
62 t.Fatalf("error = %q, want it to contain %q", err, c.wantError)
63 }
64 return
65 }
66 if err != nil {
67 t.Fatalf("network %q on a %s runner: %v", c.job, c.runner, err)
68 }
69 if cfg.Network != c.want {
70 t.Fatalf("network %q on a %s runner = %q, want %q", c.job, c.runner, cfg.Network, c.want)
71 }
72 })
73 }
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.
79func 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
86cmd="$1"; shift
87state="${FAKE_STATE:?}"
88case "$cmd" in
89run)
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 ;;
99logs)
100 printf 'db says hi\n'
101 printf 'logs %s\n' "$1" >> "$state/calls"
102 ;;
103rm)
104 for last in "$@"; do :; done
105 printf 'rm %s\n' "$last" >> "$state/calls"
106 ;;
107network)
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 ;;
125esac
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
172func TestCleanupStaleNetworksSkipsALiveRunner(t *testing.T) {
173 dir := t.TempDir()
174 state := filepath.Join(dir, "calls")
175 script := `#!/bin/sh
176cmd="$1"; shift
177case "$cmd" in
178network)
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 ;;
196esac
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}
217
218// A runtime whose binary is on PATH but which cannot create a container is the
219// machine's problem, not the repository's: the probe runs before the first lease, so
220// no job is taken and none is recorded as a failure of a commit that never ran a
221// step.
222func TestRunRefusesARuntimeThatCannotCreateAContainer(t *testing.T) {
223 dir := t.TempDir()
224 script := `#!/bin/sh
225echo 'Error: default OCI runtime "crun" not found: invalid argument' >&2
226exit 125
227`
228 if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil {
229 t.Fatal(err)
230 }
231 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
232
233 err := Run(context.Background(), Options{
234 Remote: "ssh://git@example.invalid:2222",
235 Dir: t.TempDir(),
236 ContainerRuntime: "docker",
237 })
238 want := `cannot run a container with docker: Error: default OCI runtime "crun" not found: invalid argument`
239 if err == nil || err.Error() != want {
240 t.Fatalf("Run = %v, want %q", err, want)
241 }
242}
243
244// The size of the job's /tmp is the operator's, and it reaches the container settings
245// the runner actually uses.
246func TestContainerOptionsCarryTheTmpfsLimit(t *testing.T) {
247 dir := t.TempDir()
248 if err := os.WriteFile(filepath.Join(dir, "docker"), []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
249 t.Fatal(err)
250 }
251 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
252
253 box, err := containerOptions(Options{ContainerRuntime: "docker", ContainerTmpfs: "3g"}, t.TempDir())
254 if err != nil {
255 t.Fatal(err)
256 }
257 if box.Tmpfs != "3g" {
258 t.Fatalf("tmpfs = %q, want 3g", box.Tmpfs)
259 }
260}
261