cmd/gitbay-runner/env_test.go

2d65be8962980c4c1c966517f54c1e8ff5d62b72
gitbay/cmd/gitbay-runner/env_test.go history · blame · raw

164 lines · 5114 bytes

  1package main
  2
  3import (
  4	"os"
  5	"strings"
  6	"testing"
  7	"time"
  8)
  9
 10// A step's environment is constructed, not inherited: repository content
 11// must not see what the operator set on the runner service (#144).
 12func TestStepEnvDoesNotInherit(t *testing.T) {
 13	t.Setenv("GITBAY_RUNNER_TOKEN", "a-secret-the-service-was-given")
 14	t.Setenv("AWS_SECRET_ACCESS_KEY", "also-not-for-builds")
 15
 16	env := stepEnv(job{Repo: "alice/app", SHA: "abc", Ref: "main", Job: "test"}, "/tmp/buildhome")
 17
 18	for _, e := range env {
 19		if strings.HasPrefix(e, "GITBAY_RUNNER_TOKEN=") || strings.HasPrefix(e, "AWS_SECRET_ACCESS_KEY=") {
 20			t.Errorf("the runner's own environment reached a build step: %q", e)
 21		}
 22	}
 23	want := map[string]string{
 24		"CI": "true", "GITBAY_REPO": "alice/app", "GITBAY_SHA": "abc",
 25		"GITBAY_REF": "main", "GITBAY_JOB": "test",
 26		// HOME is the shared build home, not the runner's own, so a
 27		// build cannot read the dotfiles where tools keep credentials —
 28		// and not the workspace, which is deleted after every build,
 29		// taking every tool cache with it.
 30		"HOME": "/tmp/buildhome",
 31	}
 32	got := map[string]string{}
 33	for _, e := range env {
 34		k, v, _ := strings.Cut(e, "=")
 35		got[k] = v
 36	}
 37	for k, v := range want {
 38		if got[k] != v {
 39			t.Errorf("%s = %q, want %q", k, got[k], v)
 40		}
 41	}
 42	if got["PATH"] == "" {
 43		t.Error("PATH is empty; a step could not find any tool")
 44	}
 45}
 46
 47// Secrets are passed through when the server sent them, which it does
 48// only for a trusted build.
 49func TestStepEnvCarriesSecrets(t *testing.T) {
 50	env := stepEnv(job{Secrets: map[string]string{"TOKEN": "s3cret"}}, "/tmp/buildhome")
 51	if !containsEnv(env, "TOKEN=s3cret") {
 52		t.Error("a trusted build's secret did not reach the step")
 53	}
 54	env = stepEnv(job{}, "/tmp/buildhome")
 55	for _, e := range env {
 56		if strings.HasPrefix(e, "TOKEN=") {
 57			t.Errorf("a secret appeared with none sent: %q", e)
 58		}
 59	}
 60}
 61
 62// PATH falls back rather than leaving a step unable to find anything.
 63func TestStepEnvPathFallback(t *testing.T) {
 64	old := os.Getenv("PATH")
 65	os.Unsetenv("PATH")
 66	defer os.Setenv("PATH", old)
 67	if env := stepEnv(job{}, "/tmp/buildhome"); !containsEnv(env, "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") {
 68		t.Errorf("no PATH fallback: %v", env)
 69	}
 70}
 71
 72func containsEnv(env []string, want string) bool {
 73	for _, e := range env {
 74		if e == want {
 75			return true
 76		}
 77	}
 78	return false
 79}
 80
 81// The build home must outlive a build. It was briefly the workspace,
 82// which run() removes when the build ends, so every build re-downloaded
 83// the Go module cache and the ~50MB sonar scanner.
 84func TestStepEnvHomeIsNotTheWorkspace(t *testing.T) {
 85	env := stepEnv(job{ID: 7}, "/var/lib/gitbay-runner/work/home")
 86	for _, e := range env {
 87		if strings.HasPrefix(e, "HOME=") && strings.Contains(e, "build-7") {
 88			t.Errorf("HOME is the per-build workspace, which is deleted after the build: %q", e)
 89		}
 90	}
 91}
 92
 93// podman runs from a system service, where the systemd cgroup manager
 94// has no user slice to work in. Every invocation must say so, or crun
 95// fails creating the container's scope (#144).
 96func TestPodmanUsesCgroupfs(t *testing.T) {
 97	r := &runner{}
 98	got := r.podmanGlobal()
 99	found := false
100	for _, f := range got {
101		if f == "--cgroup-manager=cgroupfs" {
102			found = true
103		}
104	}
105	if !found {
106		t.Errorf("podmanGlobal() = %v, missing the cgroupfs manager", got)
107	}
108}
109
110// The build home is where caches live, so the container must see it at
111// the path HOME names; otherwise every containerised build starts cold.
112func TestEnvHomeFindsHome(t *testing.T) {
113	if got := envHome([]string{"PATH=/bin", "HOME=/var/lib/gitbay-runner/work/home", "CI=true"}); got != "/var/lib/gitbay-runner/work/home" {
114		t.Errorf("envHome = %q", got)
115	}
116	if got := envHome([]string{"PATH=/bin"}); got != "" {
117		t.Errorf("envHome with no HOME = %q, want empty", got)
118	}
119}
120
121// Closing stop drains: the build in flight finishes and is reported, and
122// no further build is claimed (#179).
123func TestServeDrainsOnStop(t *testing.T) {
124	stop := make(chan struct{})
125	started := make(chan struct{})
126	release := make(chan struct{})
127	calls := 0
128	r := &runner{stepFn: func() (bool, error) {
129		calls++
130		if calls == 1 {
131			close(started)
132			<-release // the build is in flight while stop closes
133		}
134		return true, nil
135	}}
136	done := make(chan struct{})
137	go func() { r.serve(1, false, time.Millisecond, stop); close(done) }()
138	<-started
139	close(stop)
140	close(release)
141	select {
142	case <-done:
143	case <-time.After(2 * time.Second):
144		t.Fatal("serve did not return after the in-flight build finished")
145	}
146	if calls != 1 {
147		t.Errorf("claimed %d builds after stop, want the one already in flight", calls-1)
148	}
149}
150
151// An idle worker leaves promptly on stop rather than sleeping out a poll.
152func TestServeStopsWhileIdle(t *testing.T) {
153	stop := make(chan struct{})
154	r := &runner{stepFn: func() (bool, error) { return false, nil }}
155	done := make(chan struct{})
156	go func() { r.serve(1, false, time.Hour, stop); close(done) }()
157	time.Sleep(20 * time.Millisecond)
158	close(stop)
159	select {
160	case <-done:
161	case <-time.After(2 * time.Second):
162		t.Fatal("idle worker did not stop")
163	}
164}