cmd/gitbay-runner/cgroup.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/cmd/gitbay-runner/cgroup.go history · blame · raw

120 lines · 4237 bytes

7 symbols in this file
  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}