cmd/gitbay-runner/env_test.go
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}