vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Ship the runner's unit and let a deployment create a runner machine
d0d4239f3f7236197c87a752d7965de238430f98 · Keenan Nemetz · 3d ago · parent dbae2e01
Ship the runner's unit and let a deployment create a runner machine The package installs one binary that is the forge, the runner, and the CLI, so it now carries a second unit. It is not enabled, and it refuses to start with a message until an operator fills in /etc/vole/runner.env: who runs builds on a machine is that machine owner's decision. The unit runs as a dedicated account, because rootless podman needs a home directory and a range of subordinate ids, and because a job should not find the forge's own account on the same disk. The arguments live in that one file rather than in the unit, split at spaces by systemd, so a single unit serves every machine. Each Terraform configuration takes runner=true and creates that machine beside the forge: its own firewall, no backups (its checkouts and caches are copies), the same key for a shell, and Cloud-init that installs the package, creates the account and its subuids, generates the runner's key, pins the instance's host key once, and enables the unit. The runner_setup output prints the three commands that let it take work, filled in with the addresses this apply created. The first-boot file cannot register the runner: the allowlist is per repository, and at first boot there are no repositories. That step stays with an instance admin, and the docs say so.
packaging/systemd/vole-runner.service +55
| @@ -0,0 +1,55 @@ | |||
| 1 | [Unit] | ||
| 2 | Description=Vole CI runner | ||
| 3 | Documentation=https://github.com/rodent-software/vole | ||
| 4 | After=network-online.target | ||
| 5 | Wants=network-online.target | ||
| 6 | |||
| 7 | # An instance that is down, or a name that does not resolve yet, is not a reason | ||
| 8 | # to give up on the machine: the runner waits and tries again. | ||
| 9 | StartLimitIntervalSec=0 | ||
| 10 | |||
| 11 | [Service] | ||
| 12 | Type=simple | ||
| 13 | |||
| 14 | # A dedicated unprivileged account with a home directory and a subuid and subgid | ||
| 15 | # range, so the container runtime is rootless. docs/deploy/runner.md has the two | ||
| 16 | # commands that set it up. The instance's own account is not this one: a job that | ||
| 17 | # escapes its container should not find the forge's data on the same machine. | ||
| 18 | User=ci-runner | ||
| 19 | Group=ci-runner | ||
| 20 | Environment=HOME=/var/lib/vole-runner | ||
| 21 | WorkingDirectory=/var/lib/vole-runner | ||
| 22 | RuntimeDirectory=vole-runner | ||
| 23 | RuntimeDirectoryMode=0700 | ||
| 24 | Environment=XDG_RUNTIME_DIR=/run/vole-runner | ||
| 25 | |||
| 26 | # The arguments live in a file so one unit serves every machine: --remote, | ||
| 27 | # --label, --dir, --known-hosts, --container-network. See the example shipped as | ||
| 28 | # /etc/vole/runner.env. | ||
| 29 | EnvironmentFile=-/etc/vole/runner.env | ||
| 30 | ExecStartPre=/bin/sh -c 'test -n "$VOLE_RUNNER_ARGS" || { echo "runner is not configured: set VOLE_RUNNER_ARGS in /etc/vole/runner.env" >&2; exit 1; }' | ||
| 31 | ExecStart=/usr/bin/vole runner $VOLE_RUNNER_ARGS | ||
| 32 | |||
| 33 | Restart=on-failure | ||
| 34 | RestartSec=10s | ||
| 35 | TimeoutStopSec=60s | ||
| 36 | |||
| 37 | # Rootless podman creates a cgroup per container, which it can only do if this | ||
| 38 | # service's own cgroup is delegated to it. | ||
| 39 | Delegate=yes | ||
| 40 | |||
| 41 | # A core dump of this process is a copy of the runner's SSH key, which leases jobs | ||
| 42 | # and reports their results. | ||
| 43 | LimitCORE=0 | ||
| 44 | LimitNOFILE=65536 | ||
| 45 | UMask=0027 | ||
| 46 | NoNewPrivileges=true | ||
| 47 | PrivateTmp=true | ||
| 48 | |||
| 49 | # Deliberately less sandboxed than vole.service: the isolation a job gets is its | ||
| 50 | # container, and the runtime needs user namespaces and a writable home to build | ||
| 51 | # one. Nothing here reaches the instance's data, because it is not on this | ||
| 52 | # machine. | ||
| 53 | |||
| 54 | [Install] | ||
| 55 | WantedBy=multi-user.target | ||