vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Require an image for every CI job
70b702649b875d841e294f5a085ca784d43e5bc4 · Keenan Nemetz · 16d ago · parent 7da9111e
Require an image for every CI job Runners now require an OCI runtime, and each workflow job must name either a registry image or an image-file definition under .vole/images. Definitions are validated at push time, pinned with the run, built locally by the runner, and cached by definition digest. Records and the run page carry the image and definition digest. Split the CI guide into workflows and runners, and add reference pages for workflow and image definition fields.
ci/agent/build.go +96
| @@ -0,0 +1,96 @@ | |||
| 1 | package agent | ||
| 2 | |||
| 3 | // Building a custom image from a .vole/images definition. | ||
| 4 | // | ||
| 5 | // The job carries the pinned definition text and its digest, so the runner does | ||
| 6 | // not need to read the repository again and cannot build a definition that changed | ||
| 7 | // after the job was queued. The image is tagged by that digest, which makes a | ||
| 8 | // second job with the same definition reuse the first build. | ||
| 9 | |||
| 10 | import ( | ||
| 11 | "context" | ||
| 12 | "fmt" | ||
| 13 | "os" | ||
| 14 | "os/exec" | ||
| 15 | "path/filepath" | ||
| 16 | "strings" | ||
| 17 | "time" | ||
| 18 | |||
| 19 | "github.com/rodent-software/vole/ci" | ||
| 20 | ) | ||
| 21 | |||
| 22 | const imageBuildTimeout = 30 * time.Minute | ||
| 23 | |||
| 24 | // resolveImage returns the reference to run. A registry image is used as written; | ||
| 25 | // an image-file definition is built locally and cached under a digest tag. | ||
| 26 | func (s *runnerSession) resolveImage(job *runnerJob, cfg containerConfig, log *strings.Builder, lease time.Duration) (string, error) { | ||
| 27 | if job.ImageFile == "" { | ||
| 28 | if err := ci.ValidImageRef(job.Image); err != nil { | ||
| 29 | return "", fmt.Errorf("the job image: %w", err) | ||
| 30 | } | ||
| 31 | return job.Image, nil | ||
| 32 | } | ||
| 33 | if job.ImageDefinition == "" { | ||
| 34 | return "", fmt.Errorf("image-file %q has no pinned definition", job.ImageFile) | ||
| 35 | } | ||
| 36 | if job.ImageSHA == "" { | ||
| 37 | return "", fmt.Errorf("image-file %q has no definition digest", job.ImageFile) | ||
| 38 | } | ||
| 39 | tag := imageTag(job) | ||
| 40 | if cfg.pull != "always" && imageExists(cfg.runtime, tag) { | ||
| 41 | fmt.Fprintf(log, "image-file %s: using cached %s\n", job.ImageFile, tag) | ||
| 42 | return tag, nil | ||
| 43 | } | ||
| 44 | |||
| 45 | dockerfile, err := ci.RenderImage(job.ImageDefinition) | ||
| 46 | if err != nil { | ||
| 47 | return "", fmt.Errorf("image-file %q: %w", job.ImageFile, err) | ||
| 48 | } | ||
| 49 | dir, err := os.MkdirTemp("", "vole-image-*") | ||
| 50 | if err != nil { | ||
| 51 | return "", err | ||
| 52 | } | ||
| 53 | defer os.RemoveAll(dir) | ||
| 54 | if err := os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(dockerfile), 0o600); err != nil { | ||
| 55 | return "", err | ||
| 56 | } | ||
| 57 | |||
| 58 | args := []string{"build", "-t", tag, "-f", filepath.Join(dir, "Dockerfile"), dir} | ||
| 59 | switch { | ||
| 60 | case cfg.pull == "always" && cfg.runtime == "podman": | ||
| 61 | args = append(args, "--pull=always") | ||
| 62 | case cfg.pull == "always": | ||
| 63 | args = append(args, "--pull") | ||
| 64 | } | ||
| 65 | ctx, cancel := context.WithTimeout(context.Background(), imageBuildTimeout) | ||
| 66 | defer cancel() | ||
| 67 | cmd := exec.CommandContext(ctx, cfg.runtime, args...) | ||
| 68 | code, out, lost := s.runCommand(ctx, cmd, job.ID, lease, nil) | ||
| 69 | log.WriteString(out) | ||
| 70 | if lost { | ||
| 71 | return "", errLeaseLost | ||
| 72 | } | ||
| 73 | if ctx.Err() == context.DeadlineExceeded { | ||
| 74 | return "", fmt.Errorf("building image-file %q timed out after %s", job.ImageFile, imageBuildTimeout) | ||
| 75 | } | ||
| 76 | if code != 0 { | ||
| 77 | return "", fmt.Errorf("building image-file %q exited %d", job.ImageFile, code) | ||
| 78 | } | ||
| 79 | if !imageExists(cfg.runtime, tag) { | ||
| 80 | return "", fmt.Errorf("building image-file %q produced no image tagged %s", job.ImageFile, tag) | ||
| 81 | } | ||
| 82 | fmt.Fprintf(log, "\nimage-file %s: built %s\n", job.ImageFile, tag) | ||
| 83 | return tag, nil | ||
| 84 | } | ||
| 85 | |||
| 86 | func imageTag(job *runnerJob) string { | ||
| 87 | sha := job.ImageSHA | ||
| 88 | if len(sha) > 16 { | ||
| 89 | sha = sha[:16] | ||
| 90 | } | ||
| 91 | return "vole/image-file:" + job.ImageFile + "-" + sha | ||
| 92 | } | ||
| 93 | |||
| 94 | func imageExists(runtime, ref string) bool { | ||
| 95 | return exec.Command(runtime, "image", "inspect", ref).Run() == nil | ||
| 96 | } | ||