cmd/gitbay-runner/cgroup_test.go

v1.43.0
gitbay/cmd/gitbay-runner/cgroup_test.go history · blame · raw

159 lines · 5175 bytes

  1package main
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"strings"
  8	"testing"
  9)
 10
 11// Podman's --memory and --cpus never applied under rootless cgroupfs: the
 12// container ran in the service's own cgroup and crun could not create a
 13// child (#188). The runner now owns the build cgroups itself and places
 14// podman inside one, so the limits are written by the runner in cgroup
 15// v2's own units.
 16func TestMemoryBytes(t *testing.T) {
 17	cases := map[string]int64{
 18		"64m":  64 << 20,
 19		"6g":   6 << 30,
 20		"512k": 512 << 10,
 21		"100b": 100,
 22		"4096": 4096,
 23		"1G":   1 << 30,
 24	}
 25	for in, want := range cases {
 26		got, err := memoryBytes(in)
 27		if err != nil || got != want {
 28			t.Errorf("memoryBytes(%q) = %d, %v; want %d", in, got, err, want)
 29		}
 30	}
 31	for _, bad := range []string{"", "lots", "6gb", "-1g", "1.5g"} {
 32		if _, err := memoryBytes(bad); err == nil {
 33			t.Errorf("memoryBytes(%q) accepted", bad)
 34		}
 35	}
 36}
 37
 38func TestCPUMax(t *testing.T) {
 39	cases := map[string]string{
 40		"3":    "300000 100000",
 41		"1":    "100000 100000",
 42		"1.5":  "150000 100000",
 43		"0.25": "25000 100000",
 44	}
 45	for in, want := range cases {
 46		got, err := cpuMax(in)
 47		if err != nil || got != want {
 48			t.Errorf("cpuMax(%q) = %q, %v; want %q", in, got, err, want)
 49		}
 50	}
 51	for _, bad := range []string{"", "0", "-1", "two"} {
 52		if _, err := cpuMax(bad); err == nil {
 53			t.Errorf("cpuMax(%q) accepted", bad)
 54		}
 55	}
 56}
 57
 58// /proc/self/cgroup on cgroup v2 is one line, "0::<path>".
 59func TestOwnCgroupPath(t *testing.T) {
 60	got, err := ownCgroupPath("0::/system.slice/gitbay-runner.service\n")
 61	if err != nil || got != "/system.slice/gitbay-runner.service" {
 62		t.Fatalf("ownCgroupPath = %q, %v", got, err)
 63	}
 64	// A v1 hierarchy, or anything else, is not something the runner
 65	// manages.
 66	if _, err := ownCgroupPath("12:memory:/user.slice\n0::/init.scope\n"); err == nil {
 67		t.Fatal("v1 hierarchy accepted")
 68	}
 69}
 70
 71// Limits land as cgroup v2 interface files in the build's directory;
 72// an unset limit writes nothing, which means "inherit", not "max".
 73func TestWriteLimits(t *testing.T) {
 74	dir := t.TempDir()
 75	if err := writeLimits(dir, "6g", "3"); err != nil {
 76		t.Fatal(err)
 77	}
 78	if got, _ := os.ReadFile(filepath.Join(dir, "memory.max")); string(got) != "6442450944" {
 79		t.Errorf("memory.max = %q", got)
 80	}
 81	if got, _ := os.ReadFile(filepath.Join(dir, "cpu.max")); string(got) != "300000 100000" {
 82		t.Errorf("cpu.max = %q", got)
 83	}
 84	empty := t.TempDir()
 85	if err := writeLimits(empty, "", ""); err != nil {
 86		t.Fatal(err)
 87	}
 88	if entries, _ := os.ReadDir(empty); len(entries) != 0 {
 89		t.Errorf("unset limits wrote %v", entries)
 90	}
 91	if err := writeLimits(t.TempDir(), "lots", ""); err == nil {
 92		t.Error("a bad memory limit was accepted")
 93	}
 94}
 95
 96// A build's cgroup sits under its trust class, which is what the builds
 97// nftables table matches on (#260).
 98func TestBuildCgroupDir(t *testing.T) {
 99	builds := "/sys/fs/cgroup/system.slice/gitbay-runner.service/builds"
100	if got := buildCgroupDir(builds, 7, true); got != builds+"/trusted/build-7" {
101		t.Errorf("trusted: %s", got)
102	}
103	if got := buildCgroupDir(builds, 8, false); got != builds+"/untrusted/build-8" {
104		t.Errorf("untrusted: %s", got)
105	}
106}
107
108// The builds table and the drop-in that creates its cgroups and loads it
109// name the same class cgroups the runner places builds in. A rename on
110// one side alone would leave builds unmatched, with only the uid table
111// between them and the host.
112func TestBuildsTableNamesTheClassCgroups(t *testing.T) {
113	read := func(name string) string {
114		t.Helper()
115		b, err := os.ReadFile(filepath.Join("..", "..", "deploy", name))
116		if err != nil {
117			t.Fatal(err)
118		}
119		return string(b)
120	}
121	const unit = "system.slice/gitbay-runner.service"
122	table, dropin := read("gitbay-runner-builds.nft"), read("gitbay-runner.override.conf")
123	for _, class := range buildClasses {
124		match := fmt.Sprintf(`socket cgroupv2 level 4 "%s/builds/%s" jump %s`, unit, class, class)
125		if !strings.Contains(table, match) {
126			t.Errorf("gitbay-runner-builds.nft lacks %q", match)
127		}
128		dir := "/sys/fs/cgroup/" + unit + "/builds/" + class
129		if !strings.Contains(dropin, dir) {
130			t.Errorf("the drop-in does not create %s", dir)
131		}
132	}
133	if !strings.Contains(dropin, "ExecStartPre=+/usr/sbin/nft -f /etc/gitbay-runner/builds.nft") {
134		t.Error("the drop-in does not load the builds table")
135	}
136}
137
138// Without build cgroups a podman runner may carry on only where nothing
139// depends on them: no limits, no untrusted builds, and not on the
140// daemon's host, where the builds table matches by cgroup (#260).
141func TestBuildCgroupsRequired(t *testing.T) {
142	cases := []struct {
143		memory, cpus        string
144		untrusted, loopback bool
145		want                string
146	}{
147		{"", "", false, false, ""},
148		{"6g", "", false, false, "-memory/-cpus"},
149		{"", "3", false, false, "-memory/-cpus"},
150		{"", "", true, false, "-untrusted"},
151		{"", "", false, true, "a loopback -remote"},
152		{"", "", true, true, "-untrusted"},
153	}
154	for _, c := range cases {
155		if got := buildCgroupsRequired(c.memory, c.cpus, c.untrusted, c.loopback); got != c.want {
156			t.Errorf("buildCgroupsRequired(%q, %q, %v, %v) = %q, want %q", c.memory, c.cpus, c.untrusted, c.loopback, got, c.want)
157		}
158	}
159}