e2e/isolation_podman_test.go

e3632a550366fe23b2619edba30fc58fab19b598
gitbay/e2e/isolation_podman_test.go history · blame · raw

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}