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.go +57

@@ -21,6 +21,7 @@
21 21 package container
22 22
23 23 import (
24 "bytes"
24 25 "context"
25 26 "encoding/json"
26 27 "fmt"
@@ -113,6 +114,62 @@ func Validate(cfg Config) (Config, error) {
113 114 return cfg, nil
114 115 }
115 116
117 // ProbeTimeout bounds the startup probe. It is generous because the first `info`
118 // on a machine that has never run a container initializes the runtime's storage;
119 // on a runner that has run a job it answers in milliseconds.
120 const ProbeTimeout = 60 * time.Second
121
122 // Probe proves the runtime can create a container, not just that its binary is on
123 // PATH, so a runner that cannot run a job refuses to serve instead of taking one
124 // and failing it.
125 //
126 // A missing OCI runtime, an unwritable run root, a storage driver the kernel will
127 // not load, and an unset or missing XDG_RUNTIME_DIR all pass the LookPath check in
128 // Validate and none of them can start a container. They are the states that make a
129 // runtime exit 125 on the first job, and every one of them makes `info` fail. It is
130 // cheap, it pulls no image, and it is the same query whether a job is run here or
131 // on a runner.
132 //
133 // The error names the runtime and repeats its own last word, because "exited 125"
134 // is what left an operator reading a job log to find out that the machine, not the
135 // repository, was broken.
136 func Probe(ctx context.Context, cfg Config) error {
137 ctx, cancel := context.WithTimeout(ctx, ProbeTimeout)
138 defer cancel()
139 // Both streams, one buffer: docker reports a daemon it cannot reach on stderr
140 // and podman writes a warning there before the error, while the report on
141 // success goes to stdout and is not read.
142 cmd := exec.CommandContext(ctx, cfg.Runtime, "info")
143 var out bytes.Buffer
144 cmd.Stdout = &out
145 cmd.Stderr = &out
146 err := cmd.Run()
147 if err == nil {
148 return nil
149 }
150 reason := lastLine(out.String())
151 if ctx.Err() == context.DeadlineExceeded {
152 reason = fmt.Sprintf("%s info did not finish within %s", cfg.Runtime, ProbeTimeout)
153 }
154 if reason == "" {
155 reason = err.Error()
156 }
157 return fmt.Errorf("cannot run a container with %s: %s", cfg.Runtime, reason)
158 }
159
160 // lastLine is the runtime's own verdict: podman ends with "Error: default OCI
161 // runtime \"crun\" not found" after any number of warnings above it, and docker
162 // says one line. The warnings are not the cause, so they are not repeated.
163 func lastLine(out string) string {
164 lines := strings.Split(out, "\n")
165 for i := len(lines) - 1; i >= 0; i-- {
166 if line := strings.TrimSpace(lines[i]); line != "" {
167 return line
168 }
169 }
170 return ""
171 }
172
116 173 // RunsAsRoot reports whether the task will run as uid 0 in its container. That is
117 174 // the one case where the image's own files are fully writable for the length of the
118 175 // job, so a caller should say so: an ordinary user is protected by the image's