e2e/isolation_podman_test.go

3c0c20da787d91bcad19aec3b5e1b5c4ac5a4745
gitbay/e2e/isolation_podman_test.go history · blame · raw

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:2", "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:2 -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:2",
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}