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.

Probe the container runtime at startup instead of on the first job

f28097af06382d809c64e8ce93e5838e465b58af · Keenan Nemetz · 3h ago · parent 9704d735

Probe the container runtime at startup instead of on the first job

Validating the runtime checked the binary: an unsupported name, or not on PATH.
That is not the same as the runtime being able to create a container. An
unwritable run root, a storage driver the kernel will not load, an OCI runtime
that is not installed, and an XDG_RUNTIME_DIR mismatch all pass a LookPath check
and none of them can start a container, so a runner came up active, connected,
leased a job, and failed it before a step ran.

The only party that can know is the machine, and the only cheap way to ask is the
runtime itself: `info` fails for each of those states, pulls no image, and answers
in milliseconds on a runner that has run a job. Probe once at startup and refuse
to serve, so the unit fails, Restart=on-failure retries it, and journalctl carries
the reason instead of a job log carrying an exit code.

The message names the runtime and repeats its own last line — podman's warnings
about the run root are not the cause, and "exited 125" is what made a machine's
problem read as a repository whose build broke:

  cannot run a container with podman: Error: default OCI runtime "crun" not found: invalid argument

vole workflow run makes the same check before a local run, so a developer with a
stopped daemon hears about it up front rather than at the first step.

The probe runs once: a runtime that breaks while a runner is serving fails the job
it was building, which is a record the run page has to be able to describe (#246),
not something a second probe would have prevented.

Closes 01M6Z05PXY0HN6B7Q1XYYH3MQC

ci/container/container_test.go +65

@@ -2,7 +2,9 @@ package container
2 2
3 3 import (
4 4 "context"
5 "os"
5 6 "os/exec"
7 "path/filepath"
6 8 "strings"
7 9 "testing"
8 10 )
@@ -296,3 +298,66 @@ func TestTailBufferKeepsTheEnd(t *testing.T) {
296 298 t.Fatalf("tail after a long write = %q, want defgh", got)
297 299 }
298 300 }
301
302 // fakeRuntime puts a script on PATH under the runtime's name, so Validate's
303 // LookPath and Probe's own invocation see a runtime without one being installed.
304 func fakeRuntime(t *testing.T, name, script string) {
305 t.Helper()
306 dir := t.TempDir()
307 if err := os.WriteFile(filepath.Join(dir, name), []byte(script), 0o755); err != nil {
308 t.Fatal(err)
309 }
310 t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
311 }
312
313 // A runtime on PATH is not a runtime that can make a container. The probe has to
314 // run it, and the error has to carry the runtime's own reason: "exited 125" is what
315 // made a broken runner look like a repository whose build failed.
316 func TestProbeReportsWhatTheRuntimeSaid(t *testing.T) {
317 fakeRuntime(t, "podman", `#!/bin/sh
318 [ "$1" = info ] || exit 99
319 echo 'level=warning msg="RunRoot is pointing to a path (/run/vole-runner/containers) which is not writable"' >&2
320 echo 'Error: default OCI runtime "crun" not found: invalid argument' >&2
321 exit 125
322 `)
323 cfg, err := Validate(Config{Runtime: "podman"})
324 if err != nil {
325 t.Fatal(err)
326 }
327 err = Probe(context.Background(), cfg)
328 want := `cannot run a container with podman: Error: default OCI runtime "crun" not found: invalid argument`
329 if err == nil || err.Error() != want {
330 t.Fatalf("Probe = %v, want %q", err, want)
331 }
332 }
333
334 func TestProbeAcceptsARuntimeThatAnswers(t *testing.T) {
335 fakeRuntime(t, "docker", `#!/bin/sh
336 [ "$1" = info ] || exit 99
337 echo "Server Version: 29.8.1"
338 `)
339 cfg, err := Validate(Config{Runtime: "docker"})
340 if err != nil {
341 t.Fatal(err)
342 }
343 if err := Probe(context.Background(), cfg); err != nil {
344 t.Fatalf("Probe on a working runtime = %v", err)
345 }
346 }
347
348 // The warning podman prints above the fatal line is not the cause. A runtime that
349 // says nothing leaves the exec error, so the message is never empty.
350 func TestLastLineSkipsWarningsAndBlankLines(t *testing.T) {
351 t.Parallel()
352 cases := map[string]string{
353 "one line\n": "one line",
354 "warning\n\nError: the real reason\n\n": "Error: the real reason",
355 "": "",
356 "\n \n": "",
357 }
358 for in, want := range cases {
359 if got := lastLine(in); got != want {
360 t.Fatalf("lastLine(%q) = %q, want %q", in, got, want)
361 }
362 }
363 }