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.

Run CI jobs in a container chosen by the runner

7da9111ecabb236d48fc48592cd3d1d45f9afe78 · Keenan Nemetz · 15d ago · parent 474a812d

Run CI jobs in a container chosen by the runner

Each lease gets a disposable checkout and, when --image is set, its own container from that image. The runner keeps the SSH key and trusted Git cache outside the job, so workflow code cannot reach either; older caches are rebuilt because a previous job may have written to them.

The record names the image and runtime, and the queue mirrors them for the run page. Without --image the runner still executes on the host and says so.

ci/agent/sandbox_test.go +86

@@ -0,0 +1,86 @@
1 package agent
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 // The container arguments are the security boundary in one list. The defaults have
9 // to stay closed: only the job tree is mounted, the root filesystem is read-only,
10 // capabilities are dropped, and no host environment or SSH agent is passed through.
11 func TestContainerRunArgsAreClosed(t *testing.T) {
12 t.Parallel()
13 cfg := containerConfig{
14 runtime: "podman",
15 image: "golang:1.27@sha256:abc",
16 network: "none",
17 pull: "missing",
18 user: "1000:1000",
19 userns: "keep-id",
20 memory: "2g",
21 cpus: "2",
22 pids: 512,
23 id: "runner1",
24 }
25 args := strings.Join(containerRunArgs(cfg, "vole-runner1-run1-1", "/work/job", "run1"), " ")
26
27 for _, want := range []string{
28 "run -d --rm",
29 "--name vole-runner1-run1-1",
30 "--workdir /workspace",
31 "--user 1000:1000",
32 "--userns keep-id",
33 "--volume /work/job:/workspace:rw",
34 "--read-only",
35 "--cap-drop ALL",
36 "--security-opt no-new-privileges",
37 "--network none",
38 "--pull missing",
39 "--pids-limit 512",
40 "--memory 2g",
41 "--cpus 2",
42 "--entrypoint /bin/sh",
43 "golang:1.27@sha256:abc",
44 } {
45 if !strings.Contains(args, want) {
46 t.Fatalf("container arguments do not contain %q:\n%s", want, args)
47 }
48 }
49 for _, unwanted := range []string{
50 "--privileged",
51 "--network host",
52 "/var/run/docker.sock",
53 "SSH_AUTH_SOCK",
54 "/home/runner",
55 } {
56 if strings.Contains(args, unwanted) {
57 t.Fatalf("container arguments contain %q:\n%s", unwanted, args)
58 }
59 }
60 if strings.Count(args, "--volume") != 1 {
61 t.Fatalf("expected exactly one bind mount, got:\n%s", args)
62 }
63 }
64
65 func TestContainerRunArgsOmitEmptyLimits(t *testing.T) {
66 t.Parallel()
67 cfg := containerConfig{
68 runtime: "docker", image: "alpine:3", network: "none", pull: "never",
69 user: "1000:1000", id: "runner1",
70 }
71 args := strings.Join(containerRunArgs(cfg, "name", "/work", "run"), " ")
72 for _, unwanted := range []string{"--memory", "--cpus", "--pids-limit"} {
73 if strings.Contains(args, unwanted) {
74 t.Fatalf("an unset limit was passed as %q:\n%s", unwanted, args)
75 }
76 }
77 }
78
79 func TestContainerOptionsNeedImage(t *testing.T) {
80 if _, err := containerOptions(Options{ContainerNetwork: "bridge"}, "/tmp/cache"); err == nil {
81 t.Fatal("container options without --image were accepted")
82 }
83 if cfg, err := containerOptions(Options{}, "/tmp/cache"); err != nil || cfg.enabled() {
84 t.Fatalf("host mode = %+v, %v", cfg, err)
85 }
86 }