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 | } | ||