cmd/gitbay-runner/cgroup.go
120 lines · 4237 bytes
1package main
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strconv"
8 "strings"
9)
10
11// Build limits are cgroup v2 files the runner writes itself. Podman's
12// --memory and --cpus never applied here: under rootless podman with the
13// cgroupfs manager the container starts inside the service's own cgroup
14// and crun cannot create a child, so the flags were accepted and ignored
15// (#188). The runner owns a cgroup per build under its delegated service
16// cgroup instead, writes the limits into it, and starts every podman
17// process for that build from inside it with podman's own cgroup handling
18// off. What follows is the portable half: parsing and the file writes.
19
20// buildClasses are the cgroups a build is placed under, by the claim's
21// trust flag. deploy/gitbay-runner-builds.nft matches a build's sockets,
22// pasta's included, by these two cgroups (#260), so the names are fixed.
23var buildClasses = []string{"trusted", "untrusted"}
24
25// buildCgroupDir is build id's cgroup under the runner's builds cgroup.
26func buildCgroupDir(builds string, id int64, trusted bool) string {
27 class := buildClasses[1]
28 if trusted {
29 class = buildClasses[0]
30 }
31 return filepath.Join(builds, class, fmt.Sprintf("build-%d", id))
32}
33
34// buildCgroupsRequired names what makes build cgroups mandatory, or ""
35// when a podman runner may run its builds in its own service cgroup.
36// Limits that cannot be applied are refused, not dropped: a runner that
37// accepted -memory and ran uncapped is what #188 was. A runner taking
38// untrusted builds, or polling over loopback on the daemon's host, is
39// the shape the builds nftables table guards, and that table matches
40// builds by these cgroups; without them it matches nothing (#260).
41func buildCgroupsRequired(memory, cpus string, untrusted, loopback bool) string {
42 switch {
43 case memory != "" || cpus != "":
44 return "-memory/-cpus"
45 case untrusted:
46 return "-untrusted"
47 case loopback:
48 return "a loopback -remote"
49 }
50 return ""
51}
52
53// memoryBytes parses podman's memory units — a whole number with an
54// optional b, k, m or g suffix — into bytes.
55func memoryBytes(s string) (int64, error) {
56 if s == "" {
57 return 0, fmt.Errorf("empty memory limit")
58 }
59 num, unit := s, ""
60 if last := s[len(s)-1]; last < '0' || last > '9' {
61 num, unit = s[:len(s)-1], strings.ToLower(s[len(s)-1:])
62 }
63 n, err := strconv.ParseInt(num, 10, 64)
64 if err != nil || n <= 0 {
65 return 0, fmt.Errorf("memory limit %q: want a whole number of b, k, m or g", s)
66 }
67 shift := map[string]uint{"": 0, "b": 0, "k": 10, "m": 20, "g": 30}
68 sh, ok := shift[unit]
69 if !ok {
70 return 0, fmt.Errorf("memory limit %q: unit %q is not b, k, m or g", s, unit)
71 }
72 return n << sh, nil
73}
74
75// cpuMax renders a CPU count, whole or fractional, as cgroup v2's
76// "<quota> <period>" over a 100ms period.
77func cpuMax(s string) (string, error) {
78 const period = 100000
79 f, err := strconv.ParseFloat(s, 64)
80 if err != nil || f <= 0 {
81 return "", fmt.Errorf("cpu limit %q: want a positive number of CPUs", s)
82 }
83 return fmt.Sprintf("%d %d", int64(f*period+0.5), period), nil
84}
85
86// ownCgroupPath reads the cgroup v2 path out of /proc/self/cgroup
87// contents. A v1 hierarchy has more than the one "0::" line and is not
88// something the runner manages.
89func ownCgroupPath(procSelfCgroup string) (string, error) {
90 lines := strings.Split(strings.TrimSpace(procSelfCgroup), "\n")
91 if len(lines) != 1 || !strings.HasPrefix(lines[0], "0::/") {
92 return "", fmt.Errorf("not a cgroup v2 host: /proc/self/cgroup is %q", strings.TrimSpace(procSelfCgroup))
93 }
94 return strings.TrimPrefix(lines[0], "0::"), nil
95}
96
97// writeLimits writes the requested limits into a cgroup directory. An
98// unset limit writes nothing, so the build inherits whatever the unit
99// allows rather than getting "max".
100func writeLimits(dir, memory, cpus string) error {
101 if memory != "" {
102 n, err := memoryBytes(memory)
103 if err != nil {
104 return err
105 }
106 if err := os.WriteFile(filepath.Join(dir, "memory.max"), []byte(strconv.FormatInt(n, 10)), 0o644); err != nil {
107 return err
108 }
109 }
110 if cpus != "" {
111 v, err := cpuMax(cpus)
112 if err != nil {
113 return err
114 }
115 if err := os.WriteFile(filepath.Join(dir, "cpu.max"), []byte(v), 0o644); err != nil {
116 return err
117 }
118 }
119 return nil
120}