e2e/isolation_podman_test.go
163 lines · 6401 bytes
1package e2e
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12// havePodman reports whether a working rootless podman is on this
13// machine. The skip is loud on purpose: an isolation test that quietly
14// does not run is how isolation regresses (#144).
15func havePodman(t *testing.T) bool {
16 t.Helper()
17 if _, err := exec.LookPath("podman"); err != nil {
18 t.Log("SKIPPING ISOLATION TEST: podman is not installed on this machine. " +
19 "The container path is NOT covered by this run.")
20 return false
21 }
22 if out, err := exec.Command("podman", "info", "--format", "{{.Host.Security.Rootless}}").CombinedOutput(); err != nil {
23 t.Logf("SKIPPING ISOLATION TEST: podman does not work here: %v\n%s", err, out)
24 return false
25 }
26 return true
27}
28
29// The fallback that must not exist: with -isolation podman and no podman,
30// the runner refuses to start rather than running a build on the host.
31// This one needs no podman, so it runs everywhere.
32func TestRunnerRefusesToStartWithoutPodman(t *testing.T) {
33 bin := buildRunner(t)
34 cmd := exec.Command(bin, "-once", "-remote", "git@127.0.0.1",
35 "-isolation", "podman", "-workdir", t.TempDir())
36 // An empty PATH is the reliable way to make podman missing whether or
37 // not this machine has one.
38 cmd.Env = []string{"PATH=" + t.TempDir(), "HOME=" + t.TempDir()}
39 out, err := cmd.CombinedOutput()
40 if err == nil {
41 t.Fatalf("the runner started without podman:\n%s", out)
42 }
43 if !strings.Contains(string(out), "podman") {
44 t.Errorf("refusal does not say podman is the problem:\n%s", out)
45 }
46 if !strings.Contains(string(out), "runner-podman-setup.sh") {
47 t.Errorf("refusal does not say how to fix it:\n%s", out)
48 }
49}
50
51// An unknown mode is refused rather than guessed at.
52func TestRunnerRefusesUnknownIsolation(t *testing.T) {
53 bin := buildRunner(t)
54 cmd := exec.Command(bin, "-once", "-remote", "git@127.0.0.1",
55 "-isolation", "chroot", "-workdir", t.TempDir())
56 out, err := cmd.CombinedOutput()
57 if err == nil {
58 t.Fatalf("an unknown isolation mode started:\n%s", out)
59 }
60 if !strings.Contains(string(out), "podman or none") {
61 t.Errorf("refusal does not name the valid modes:\n%s", out)
62 }
63}
64
65// With podman, a step runs in a container: it cannot read the runner's
66// SSH key, and it does not see the runner's home.
67func TestPodmanStepCannotReachTheRunnersKey(t *testing.T) {
68 if !havePodman(t) {
69 t.Skip("no podman")
70 }
71 inst := startInstance(t)
72 inst.runner = buildRunner(t)
73 aliceKey := inst.newKey(t, "alice")
74 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
75 runnerKey := inst.newKey(t, "ci")
76 inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
77 inst.ssh(t, aliceKey, "", "repo", "create", "alice/app")
78
79 env := inst.gitEnv(aliceKey)
80 work := t.TempDir()
81 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
82 dir := filepath.Join(work, "w")
83 os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
84 // The step tries to read the key the runner authenticates with, and
85 // to list the runner's home. Both must fail inside the container.
86 os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte(
87 "jobs:\n peek:\n image: docker.io/library/debian:stable-slim\n steps:\n"+
88 " - 'if cat "+runnerKey+" 2>/dev/null; then echo LEAKED-KEY; exit 1; fi; echo no-key'\n"+
89 " - 'echo HOME=$HOME; ls /workspace'\n"), 0o644)
90 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
91 mustGit(t, dir, env, "add", ".")
92 mustGit(t, dir, env, "commit", "-q", "-m", "base")
93 mustGit(t, dir, env, "push", "-q", "origin", "main")
94
95 runnerPodmanOnce(t, inst, runnerKey)
96 out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app")
97 if !strings.Contains(out, "success") {
98 t.Fatalf("the containerised build did not pass:\n%s", out)
99 }
100 log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/app", "1")
101 if strings.Contains(log, "LEAKED-KEY") {
102 t.Errorf("a step read the runner's ssh key:\n%s", log)
103 }
104 if !strings.Contains(log, "no-key") {
105 t.Errorf("the step did not run as expected:\n%s", log)
106 }
107}
108
109// A pull failure fails the build and says why, rather than retrying or
110// silently choosing another image.
111func TestPodmanPullFailureFailsTheBuild(t *testing.T) {
112 if !havePodman(t) {
113 t.Skip("no podman")
114 }
115 inst := startInstance(t)
116 inst.runner = buildRunner(t)
117 aliceKey := inst.newKey(t, "alice")
118 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
119 runnerKey := inst.newKey(t, "ci")
120 inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
121 inst.ssh(t, aliceKey, "", "repo", "create", "alice/app")
122
123 env := inst.gitEnv(aliceKey)
124 work := t.TempDir()
125 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
126 dir := filepath.Join(work, "w")
127 os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
128 os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte(
129 "jobs:\n nope:\n image: localhost/gitbay-no-such-image:v0\n steps:\n - echo unreachable\n"), 0o644)
130 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
131 mustGit(t, dir, env, "add", ".")
132 mustGit(t, dir, env, "commit", "-q", "-m", "base")
133 mustGit(t, dir, env, "push", "-q", "origin", "main")
134
135 runnerPodmanOnce(t, inst, runnerKey)
136 out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app")
137 if !strings.Contains(out, "failure") {
138 t.Fatalf("a build with an unpullable image did not fail:\n%s", out)
139 }
140 log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/app", "1")
141 if !strings.Contains(log, "gitbay-no-such-image") {
142 t.Errorf("the log does not name the image that could not be pulled:\n%s", log)
143 }
144 if strings.Contains(log, "unreachable") {
145 t.Error("a step ran despite the image failing to start")
146 }
147}
148
149func runnerPodmanOnce(t *testing.T, inst *instance, key string) {
150 t.Helper()
151 opts := fmt.Sprintf("-p %d -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=%s -o BatchMode=yes",
152 inst.port, key, filepath.Join(inst.sshDir, "known_hosts"))
153 cmd := exec.Command(inst.runner, "-once",
154 "-remote", "git@127.0.0.1",
155 "-ssh-opts", opts,
156 "-isolation", "podman",
157 "-clone-base", fmt.Sprintf("ssh://git@127.0.0.1:%d", inst.port),
158 "-workdir", t.TempDir())
159 cmd.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null")
160 if out, err := cmd.CombinedOutput(); err != nil {
161 t.Fatalf("runner: %v\n%s", err, out)
162 }
163}