e2e/isolation_podman_test.go
205 lines · 8116 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).
15// provisionedImage returns an image present on this host, since the
16// runner never pulls one (#144). Tests must use what is provisioned, the
17// same rule builds follow.
18func provisionedImage(t *testing.T) string {
19 t.Helper()
20 for _, img := range []string{"localhost/gitbay-ci:1", "docker.io/library/debian:stable-slim", "docker.io/library/alpine:latest"} {
21 if err := exec.Command("podman", "image", "exists", img).Run(); err == nil {
22 return img
23 }
24 }
25 t.Log("SKIPPING ISOLATION TEST: podman has no image this test can use. " +
26 "Provision one (podman build -t localhost/gitbay-ci:1 -f deploy/Containerfile.ci). " +
27 "The container path is NOT covered by this run.")
28 return ""
29}
30
31func havePodman(t *testing.T) bool {
32 t.Helper()
33 if _, err := exec.LookPath("podman"); err != nil {
34 t.Log("SKIPPING ISOLATION TEST: podman is not installed on this machine. " +
35 "The container path is NOT covered by this run.")
36 return false
37 }
38 if out, err := exec.Command("podman", "info", "--format", "{{.Host.Security.Rootless}}").CombinedOutput(); err != nil {
39 t.Logf("SKIPPING ISOLATION TEST: podman does not work here: %v\n%s", err, out)
40 return false
41 }
42 return true
43}
44
45// The fallback that must not exist: with -isolation podman and no podman,
46// the runner refuses to start rather than running a build on the host.
47// This one needs no podman, so it runs everywhere.
48func TestRunnerRefusesToStartWithoutPodman(t *testing.T) {
49 bin := buildRunner(t)
50 cmd := exec.Command(bin, "-once", "-remote", "git@127.0.0.1",
51 "-isolation", "podman", "-image", "localhost/whatever:1", "-workdir", t.TempDir())
52 // An empty PATH is the reliable way to make podman missing whether or
53 // not this machine has one.
54 cmd.Env = []string{"PATH=" + t.TempDir(), "HOME=" + t.TempDir()}
55 out, err := cmd.CombinedOutput()
56 if err == nil {
57 t.Fatalf("the runner started without podman:\n%s", out)
58 }
59 if !strings.Contains(string(out), "podman") {
60 t.Errorf("refusal does not say podman is the problem:\n%s", out)
61 }
62 if !strings.Contains(string(out), "runner-podman-setup.sh") {
63 t.Errorf("refusal does not say how to fix it:\n%s", out)
64 }
65}
66
67// An unknown mode is refused rather than guessed at.
68func TestRunnerRefusesUnknownIsolation(t *testing.T) {
69 bin := buildRunner(t)
70 cmd := exec.Command(bin, "-once", "-remote", "git@127.0.0.1",
71 "-isolation", "chroot", "-workdir", t.TempDir())
72 out, err := cmd.CombinedOutput()
73 if err == nil {
74 t.Fatalf("an unknown isolation mode started:\n%s", out)
75 }
76 if !strings.Contains(string(out), "podman or none") {
77 t.Errorf("refusal does not name the valid modes:\n%s", out)
78 }
79}
80
81// With podman, a step runs in a container: it cannot read the runner's
82// SSH key, and it does not see the runner's home.
83func TestPodmanStepCannotReachTheRunnersKey(t *testing.T) {
84 if !havePodman(t) {
85 t.Skip("no podman")
86 }
87 inst := startInstance(t)
88 inst.runner = buildRunner(t)
89 aliceKey := inst.newKey(t, "alice")
90 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
91 runnerKey := inst.newKey(t, "ci")
92 inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
93 inst.ssh(t, aliceKey, "", "repo", "create", "alice/app")
94
95 image := provisionedImage(t)
96 if image == "" {
97 t.Skip("no provisioned image")
98 }
99 env := inst.gitEnv(aliceKey)
100 work := t.TempDir()
101 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
102 dir := filepath.Join(work, "w")
103 os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
104 // The step tries to read the key the runner authenticates with, and
105 // to list the runner's home. Both must fail inside the container.
106 os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte(
107 "jobs:\n peek:\n image: "+image+"\n steps:\n"+
108 " - 'if cat "+runnerKey+" 2>/dev/null; then echo LEAKED-KEY; exit 1; fi; echo no-key'\n"+
109 " - 'echo HOME=$HOME; ls /workspace'\n"), 0o644)
110 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
111 mustGit(t, dir, env, "add", ".")
112 mustGit(t, dir, env, "commit", "-q", "-m", "base")
113 mustGit(t, dir, env, "push", "-q", "origin", "main")
114
115 runnerPodmanOnce(t, inst, runnerKey)
116 out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app")
117 log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/app", "1")
118 if !strings.Contains(out, "success") {
119 // Without the log this says only "it failed", which cost two CI
120 // rounds to diagnose the first time.
121 t.Fatalf("the containerised build did not pass:\n%s\nbuild log:\n%s", out, log)
122 }
123 if strings.Contains(log, "LEAKED-KEY") {
124 t.Errorf("a step read the runner's ssh key:\n%s", log)
125 }
126 if !strings.Contains(log, "no-key") {
127 t.Errorf("the step did not run as expected:\n%s", log)
128 }
129}
130
131// An image this runner does not have fails the build and says an
132// operator must provision it, rather than pulling it.
133func TestPodmanMissingImageFailsTheBuild(t *testing.T) {
134 if !havePodman(t) {
135 t.Skip("no podman")
136 }
137 inst := startInstance(t)
138 inst.runner = buildRunner(t)
139 aliceKey := inst.newKey(t, "alice")
140 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
141 runnerKey := inst.newKey(t, "ci")
142 inst.admin(t, "admin", "user", "create", "ci", "--key", runnerKey+".pub", "--admin")
143 inst.ssh(t, aliceKey, "", "repo", "create", "alice/app")
144
145 env := inst.gitEnv(aliceKey)
146 work := t.TempDir()
147 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
148 dir := filepath.Join(work, "w")
149 os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
150 os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte(
151 "jobs:\n nope:\n image: localhost/gitbay-no-such-image:v0\n steps:\n - echo unreachable\n"), 0o644)
152 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
153 mustGit(t, dir, env, "add", ".")
154 mustGit(t, dir, env, "commit", "-q", "-m", "base")
155 mustGit(t, dir, env, "push", "-q", "origin", "main")
156
157 runnerPodmanOnce(t, inst, runnerKey)
158 out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app")
159 if !strings.Contains(out, "failure") {
160 t.Fatalf("a build with an unpullable image did not fail:\n%s", out)
161 }
162 log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/app", "1")
163 if !strings.Contains(log, "gitbay-no-such-image") {
164 t.Errorf("the log does not name the missing image:\n%s", log)
165 }
166 if !strings.Contains(log, "does not pull images") {
167 t.Errorf("the log does not say an operator must provision it:\n%s", log)
168 }
169 if strings.Contains(log, "unreachable") {
170 t.Error("a step ran despite the image failing to start")
171 }
172}
173
174func runnerPodmanOnce(t *testing.T, inst *instance, key string) {
175 t.Helper()
176 opts := fmt.Sprintf("-p %d -i %s -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=%s -o BatchMode=yes",
177 inst.port, key, filepath.Join(inst.sshDir, "known_hosts"))
178 cmd := exec.Command(inst.runner, "-once",
179 "-remote", "git@127.0.0.1",
180 "-ssh-opts", opts,
181 "-isolation", "podman",
182 "-image", "localhost/gitbay-ci:1",
183 "-clone-base", fmt.Sprintf("ssh://git@127.0.0.1:%d", inst.port),
184 "-workdir", t.TempDir())
185 cmd.Env = append(os.Environ(), "XDG_CONFIG_HOME="+t.TempDir(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null")
186 if out, err := cmd.CombinedOutput(); err != nil {
187 t.Fatalf("runner: %v\n%s", err, out)
188 }
189}
190
191// Under podman the runner insists on a default image rather than
192// guessing one: with --pull=never an image the host does not have fails
193// every job that names none.
194func TestRunnerRefusesPodmanWithoutAnImage(t *testing.T) {
195 bin := buildRunner(t)
196 cmd := exec.Command(bin, "-once", "-remote", "git@127.0.0.1",
197 "-isolation", "podman", "-workdir", t.TempDir())
198 out, err := cmd.CombinedOutput()
199 if err == nil {
200 t.Fatalf("the runner started in podman mode with no -image:\n%s", out)
201 }
202 if !strings.Contains(string(out), "-image") {
203 t.Errorf("refusal does not name the missing flag:\n%s", out)
204 }
205}