cmd/gitbay-runner/cgroup.go
87 lines · 2937 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// memoryBytes parses podman's memory units — a whole number with an
21// optional b, k, m or g suffix — into bytes.
22func memoryBytes(s string) (int64, error) {
23 if s == "" {
24 return 0, fmt.Errorf("empty memory limit")
25 }
26 num, unit := s, ""
27 if last := s[len(s)-1]; last < '0' || last > '9' {
28 num, unit = s[:len(s)-1], strings.ToLower(s[len(s)-1:])
29 }
30 n, err := strconv.ParseInt(num, 10, 64)
31 if err != nil || n <= 0 {
32 return 0, fmt.Errorf("memory limit %q: want a whole number of b, k, m or g", s)
33 }
34 shift := map[string]uint{"": 0, "b": 0, "k": 10, "m": 20, "g": 30}
35 sh, ok := shift[unit]
36 if !ok {
37 return 0, fmt.Errorf("memory limit %q: unit %q is not b, k, m or g", s, unit)
38 }
39 return n << sh, nil
40}
41
42// cpuMax renders a CPU count, whole or fractional, as cgroup v2's
43// "<quota> <period>" over a 100ms period.
44func cpuMax(s string) (string, error) {
45 const period = 100000
46 f, err := strconv.ParseFloat(s, 64)
47 if err != nil || f <= 0 {
48 return "", fmt.Errorf("cpu limit %q: want a positive number of CPUs", s)
49 }
50 return fmt.Sprintf("%d %d", int64(f*period+0.5), period), nil
51}
52
53// ownCgroupPath reads the cgroup v2 path out of /proc/self/cgroup
54// contents. A v1 hierarchy has more than the one "0::" line and is not
55// something the runner manages.
56func ownCgroupPath(procSelfCgroup string) (string, error) {
57 lines := strings.Split(strings.TrimSpace(procSelfCgroup), "\n")
58 if len(lines) != 1 || !strings.HasPrefix(lines[0], "0::/") {
59 return "", fmt.Errorf("not a cgroup v2 host: /proc/self/cgroup is %q", strings.TrimSpace(procSelfCgroup))
60 }
61 return strings.TrimPrefix(lines[0], "0::"), nil
62}
63
64// writeLimits writes the requested limits into a cgroup directory. An
65// unset limit writes nothing, so the build inherits whatever the unit
66// allows rather than getting "max".
67func writeLimits(dir, memory, cpus string) error {
68 if memory != "" {
69 n, err := memoryBytes(memory)
70 if err != nil {
71 return err
72 }
73 if err := os.WriteFile(filepath.Join(dir, "memory.max"), []byte(strconv.FormatInt(n, 10)), 0o644); err != nil {
74 return err
75 }
76 }
77 if cpus != "" {
78 v, err := cpuMax(cpus)
79 if err != nil {
80 return err
81 }
82 if err := os.WriteFile(filepath.Join(dir, "cpu.max"), []byte(v), 0o644); err != nil {
83 return err
84 }
85 }
86 return nil
87}