Commit 89eba6adea

89eba6adea7aca334fa198418e227da374980c24

parent: 6bea6f3df8

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-07 02:27 UTC

runner, deploy, wiki: -cpus and -memory cap a build's container

Unset means uncapped. bay1 runs -cpus 3 of 4 and no memory cap, since
the e2e suite needs most of the host's memory.

Ref #144

Layout: unified · split

.gitbay/wiki/Admin.org +4
@@ -438,6 +438,10 @@ because an image this host does not have would fail every build. A job
438438overrides it with =image:= in =.gitbay/ci.yml=, validated as a reference
439439so a config file cannot turn it into podman arguments.
440440
441=-cpus= and =-memory= cap one build's container (podman's own units,
442e.g. =-cpus 2 -memory 4g=); unset means uncapped. bay1 runs =-cpus 3=
443and no memory cap, since the e2e suite needs most of the host's 7GB.
444
441445*Images are provisioned, never pulled by a build.* The runner passes
442446=--pull=never=. Two reasons, and the second is the better one: the
443447service runs with =RestrictSUIDSGID=yes= so podman cannot unpack a layer
cmd/gitbay-runner/env_test.go +13
@@ -116,3 +116,16 @@ func TestEnvHomeFindsHome(t *testing.T) {
116116 t.Errorf("envHome with no HOME = %q, want empty", got)
117117 }
118118}
119
120// A limit is passed to podman only when set; unset means uncapped, not a
121// default that could kill the suite.
122func TestLimitArgs(t *testing.T) {
123 if got := (&runner{}).limitArgs(); len(got) != 0 {
124 t.Errorf("no limits set, got %v", got)
125 }
126 got := (&runner{memory: "4g", cpus: "2"}).limitArgs()
127 want := []string{"--memory", "4g", "--cpus", "2"}
128 if strings.Join(got, " ") != strings.Join(want, " ") {
129 t.Errorf("limitArgs = %v, want %v", got, want)
130 }
131}
cmd/gitbay-runner/isolate.go +23 −4
@@ -125,8 +125,9 @@ func (r *runner) runStepsPodman(j job, dir string, env []string, sink io.Writer,
125125 name := fmt.Sprintf("gitbay-build-%d", j.ID)
126126 // --rm so a container cannot outlive its build; the explicit rm below
127127 // covers the case where the daemon-less run itself fails.
128 start := exec.Command(podman, append(r.podmanGlobal(), "run", "--detach", "--rm",
129 "--pull=never",
128 args := append(r.podmanGlobal(), "run", "--detach", "--rm", "--pull=never")
129 args = append(args, r.limitArgs()...)
130 args = append(args,
130131 "--name", name,
131132 "--env-file", envFile,
132133 "--volume", dir+":/workspace:rw",
@@ -138,7 +139,8 @@ func (r *runner) runStepsPodman(j job, dir string, env []string, sink io.Writer,
138139 "--volume", envHome(env)+":"+envHome(env)+":rw",
139140 "--workdir", "/workspace",
140141 "--entrypoint", "sh",
141 image, "-c", "sleep infinity")...)
142 image, "-c", "sleep infinity")
143 start := exec.Command(podman, args...)
142144 start.Env = []string{"PATH=" + os.Getenv("PATH"), "HOME=" + r.podmanHome()}
143145 if out, err := start.CombinedOutput(); err != nil {
144146 // A missing image lands here, and it is the common case worth
@@ -187,7 +189,24 @@ func (r *runner) podmanGlobal() []string {
187189 return []string{"--cgroup-manager=cgroupfs"}
188190}
189191
190// env_home returns the HOME the step environment carries.
192// limitArgs caps one build's container. The service's CPUWeight and
193// IOWeight shape the service against other services, not one build
194// against the host, and the threat model lists resource exhaustion as
195// unaddressed. Memory is deliberately uncapped by default: the e2e suite
196// peaks past 5GB on a 7GB host, and a cap that kills the suite is an
197// outage, not a limit.
198func (r *runner) limitArgs() []string {
199 var args []string
200 if r.memory != "" {
201 args = append(args, "--memory", r.memory)
202 }
203 if r.cpus != "" {
204 args = append(args, "--cpus", r.cpus)
205 }
206 return args
207}
208
209// envHome returns the HOME the step environment carries.
191210func envHome(env []string) string {
192211 for _, e := range env {
193212 if strings.HasPrefix(e, "HOME=") {
cmd/gitbay-runner/main.go +7
@@ -49,6 +49,9 @@ type runner struct {
4949 // isolation selects how steps run: "podman" or "none".
5050 image string
5151 isolation string
52 // memory and cpus cap one build's container; empty means no cap.
53 memory string
54 cpus string
5255 // repos limits which repositories this runner claims builds for. Empty
5356 // means any, which is what a runner on the server itself wants; a runner
5457 // somewhere that should not execute every repository's steps names them.
@@ -68,6 +71,8 @@ func main() {
6871 jobs = flag.Int("jobs", 1, "builds to run at once")
6972 image = flag.String("image", "", "default container image for jobs that name none")
7073 isolation = flag.String("isolation", "podman", "how steps run: podman, or none for no container")
74 memory = flag.String("memory", "", "memory limit per build container, e.g. 4g (podman only; default unlimited)")
75 cpus = flag.String("cpus", "", "CPU limit per build container, e.g. 2 (podman only; default unlimited)")
7176 version = flag.Bool("version", false, "print the commit this binary was built from, then exit")
7277 )
7378 flag.Parse()
@@ -85,6 +90,8 @@ func main() {
8590 timeout: *timeout,
8691 image: *image,
8792 isolation: *isolation,
93 memory: *memory,
94 cpus: *cpus,
8895 }
8996 if err := r.checkIsolation(); err != nil {
9097 // Refusing to start is the point. A runner that quietly fell back
deploy/gitbay-runner.override.conf +5 −1
@@ -23,6 +23,10 @@
2323# would otherwise make read-only. Prepare the host with
2424# deploy/runner-podman-setup.sh before deploying a runner that isolates.
2525[Service]
26# -cpus 3 of the host's 4 leaves a core for gitbayd and sshd while a build
27# runs. No -memory: the e2e suite peaks past 5GB of the 7GB, and a cap
28# that kills it is an outage rather than a limit (#144).
29#
2630# ExecStart is overridden here rather than left in the unit so the flags
2731# and the sandboxing that has to match them live in one file: -isolation
2832# podman needs NoNewPrivileges=no below, and -image needs an image the
@@ -37,7 +41,7 @@
3741# End it with the service.
3842ExecStopPost=-/usr/bin/pkill -u ci-runner -x catatonit
3943ExecStart=
40ExecStart=/usr/local/bin/gitbay-runner -remote git@127.0.0.1 -workdir /var/lib/gitbay-runner/work -poll 5s -timeout 45m -repos krz/gitbay -isolation podman -image localhost/gitbay-ci:1
44ExecStart=/usr/local/bin/gitbay-runner -remote git@127.0.0.1 -workdir /var/lib/gitbay-runner/work -poll 5s -timeout 45m -repos krz/gitbay -isolation podman -image localhost/gitbay-ci:1 -cpus 3
4145Nice=10
4246CPUWeight=30
4347IOWeight=30