- Rust 60.9%
- Shell 37.8%
- Go Template 0.7%
- Dockerfile 0.6%
|
Some checks failed
Container disks / lint (push) Successful in 16s
CI / chart (push) Successful in 6s
Container disks / alpine (3.24) (push) Successful in 1m7s
Container disks / openbsd (7.9) (push) Failing after 2m32s
Container disks / freebsd (15.1) (push) Successful in 2m32s
CI / rust (push) Successful in 3m40s
Container disks / debian (trixie) (push) Successful in 2m59s
CI / build-chart (push) Successful in 6s
CI / build-image (push) Failing after 1m6s
|
||
|---|---|---|
| .forgejo/workflows | ||
| charts/forgejo-runner-controller | ||
| controller | ||
| examples | ||
| vms | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitignore | ||
| Dockerfile | ||
| LICENSE | ||
| README.md | ||
| renovate.json | ||
| rustfmt.toml | ||
forgejo-ephemeral-runner
A Kubernetes operator that runs Forgejo Actions jobs on throwaway runners. Every queued job gets its own runner, which registers, executes exactly that one job, and disappears. Nothing is reused between jobs.
Two kinds of runner are supported, chosen per Runner resource:
- pod -- a
batch/v1Job. The job image is independent of the runner: an init container copies theforgejo-runnerbinary out ofspec.runnerImage-- or the operator's own default, set chart-wide -- into anemptyDirthat the job container runs it from, so jobs can use any image at all. - vm -- a KubeVirt
VirtualMachinebooting a prepared guest image. This is how non-Linux jobs (OpenBSD, FreeBSD) get real hardware-level isolation;vms/holds the image builders.
How it works
The controller polls GET /api/v1/admin/actions/runners/jobs for jobs whose
runs-on labels a Runner serves. Forgejo has no webhook for "job queued" and
no queue-depth metric, so polling is the only option.
Every child is the same thing -- an ephemeral runner with a random name that
registers, takes one job and exits -- so the decision is arithmetic. Count the
children that can still take a job, and spawn enough to cover the queue plus
minRunners spare, without passing maxRunners:
spawn = ((waiting + minRunners) - available).min(maxRunners - live)
Children that have not connected yet count as available, which is what keeps the
loop honest: a job polled twice before its runner comes up does not get a second
one. minRunners sits on top of demand rather than being consumed by it, so a
spare stays ready even while every other child is busy.
Nothing reclaims a child early. One left behind by a cancelled job holds its slot
until jobTimeoutSeconds, which is also the backstop for anything else wedged.
Credentials are minted per spawn: the controller registers an ephemeral runner and puts the result in a Secret owned by the child, so deleting the child takes the token with it.
How the runner is then told what to do differs by kind. A pod gets an explicit
one-job --url ... --uuid ... --label ... argv on the binary in the emptyDir,
and the Secret holds only the token file it reads. A VM has no such channel --
it just boots -- so the Secret is attached as a CDROM and carries the whole
invocation as files:
| file | contents |
|---|---|
url |
Forgejo base URL |
uuid |
runner uuid from registration |
token |
runner token from registration |
label |
one <name>:host label per line |
Reading those and running forgejo-runner one-job is the guest image's job, not
the controller's. vms/bin/<os>_image.sh bakes it in: -b installs a script that
runs on every boot, -F adds the fstab entry that mounts the CDROM, and -f
copies in any extra files.
Usage
kubectl create namespace forgejo-runner
kubectl -n forgejo-runner create secret generic forgejo-pat --from-literal=token=<admin-pat>
helm install forgejo-runner-controller charts/forgejo-runner-controller
kubectl apply -f examples/runner-pod.yaml
kubectl -n forgejo-runner get runners
The token must be a Forgejo admin token: the controller registers runners instance-wide and lists queued jobs across all repositories.
See examples/ for a pod-mode and a VM-mode Runner, and the CRD in
charts/forgejo-runner-controller/crds/ for the full field reference.
Development
cd controller
cargo test
cargo clippy --all-targets -- -D warnings
cargo run --bin crdgen -- --update # regenerate the CRD in the chart
cargo run --bin forgejo-runner-controller
crdgen writes the CRD into the chart; CI fails if it is out of date.