Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

deploy/cloud-init/runner.yaml +126

@@ -0,0 +1,126 @@
1 #cloud-config
2 # A Vole CI runner on a fresh Ubuntu or Debian machine.
3 #
4 # A runner leases jobs over SSH, executes each one in a container, and reports the
5 # result. It is a separate machine from the instance on purpose: it runs
6 # repository code, and a runner can read every secret its jobs declare, so it
7 # should not share a disk with the account database and the SSH host key.
8 #
9 # Each placeholder below is a value the caller supplies. Terraform renders this
10 # file with templatefile(); deploy/cloud-init/render.sh renders it with
11 # envsubst(). Both read a placeholder as a dollar sign followed by a name in
12 # braces, so shell variables in the script at the bottom are written without
13 # braces and are left alone.
14
15 hostname: ${runner_hostname}
16 fqdn: ${runner_hostname}
17 manage_etc_hosts: true
18
19 package_update: true
20 packages:
21 - ca-certificates
22 - curl
23 - git
24 - openssh-client
25 # Rootless podman and what it needs to map the runner's user into the container
26 # and to give a container a network when a job asks for one.
27 - podman
28 - slirp4netns
29 - fuse-overlayfs
30 - uidmap
31
32 write_files:
33 # Staged, not installed directly: the package ships /etc/vole/runner.env as a
34 # conffile, so it has to arrive after the package does.
35 - path: /opt/vole-runner/runner.env
36 permissions: "0644"
37 content: |
38 # Read by vole-runner.service. One line, split at spaces.
39 VOLE_RUNNER_ARGS=--remote ssh://git@${forge_host}:2222 --container-runtime ${runner_runtime} --container-network ${runner_network} --dir ${runner_dir} --known-hosts /etc/vole/runner_known_hosts ${runner_labels}
40
41 - path: /usr/local/sbin/vole-runner-first-boot
42 permissions: "0755"
43 content: |
44 #!/bin/sh
45 # One-time setup for a Vole CI runner. cloud-init runs this once, on first
46 # boot, and it is safe to run again: the package install is skipped when
47 # vole is already there, and the account and key are only created if they
48 # are missing.
49 set -eu
50
51 FORGE_HOST='${forge_host}'
52 PACKAGE_URL='${vole_package_url}'
53 RUNNER_DIR='${runner_dir}'
54 RUNNER_USER=ci-runner
55 LOG=/var/log/vole-runner-first-boot.log
56
57 exec >>"$LOG" 2>&1
58 echo "== vole runner first boot $(date -u +%Y-%m-%dT%H:%M:%SZ) =="
59
60 # The package installs the binary, the runner unit, and /etc/vole/runner.env.
61 # It also prepares a server instance that this machine never starts.
62 if ! command -v vole >/dev/null 2>&1; then
63 echo "downloading $PACKAGE_URL"
64 curl -fsSL --retry 5 --retry-delay 2 -o /tmp/vole.deb "$PACKAGE_URL"
65 DEBIAN_FRONTEND=noninteractive apt-get install -y /tmp/vole.deb
66 rm -f /tmp/vole.deb
67 fi
68
69 # A dedicated account with a home directory and a range of subordinate ids,
70 # which is what lets the container runtime be rootless. It is not the vole
71 # service account: a job runs as this user, and the forge's own account does
72 # not exist on this machine.
73 if ! getent passwd "$RUNNER_USER" >/dev/null 2>&1; then
74 useradd --create-home --home-dir "$RUNNER_DIR" --shell /usr/sbin/nologin \
75 --comment "Vole CI runner" "$RUNNER_USER"
76 fi
77 if ! grep -q "^$RUNNER_USER:" /etc/subuid; then
78 usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$RUNNER_USER"
79 fi
80
81 # The runner authenticates with this key. Its public half is what an
82 # instance admin registers as a bot account; nothing here talks to the
83 # instance as an admin.
84 if [ ! -f "$RUNNER_DIR/.ssh/id_ed25519" ]; then
85 install -d -o "$RUNNER_USER" -g "$RUNNER_USER" -m 0700 "$RUNNER_DIR/.ssh"
86 su -s /bin/sh "$RUNNER_USER" -c "ssh-keygen -q -t ed25519 -N '' -C 'vole-runner@${runner_hostname}' -f $RUNNER_DIR/.ssh/id_ed25519"
87 fi
88
89 # Pin the instance's host key once. A runner executes repository code, so a
90 # host that can answer in the instance's place could send it any job; this
91 # file is the boundary, and it is never re-scanned on later starts.
92 #
93 # The instance may still be starting, or its name may not resolve yet, so
94 # this waits for it instead of failing the boot.
95 if [ ! -s /etc/vole/runner_known_hosts ]; then
96 echo "waiting for $FORGE_HOST on port 2222"
97 i=0
98 while [ "$i" -lt 60 ]; do
99 if ssh-keyscan -T 5 -p 2222 "$FORGE_HOST" >/tmp/known_hosts 2>/dev/null && [ -s /tmp/known_hosts ]; then
100 install -m 0644 /tmp/known_hosts /etc/vole/runner_known_hosts
101 echo "pinned the host key of $FORGE_HOST"
102 break
103 fi
104 i=$((i + 1))
105 sleep 10
106 done
107 rm -f /tmp/known_hosts
108 fi
109
110 install -m 0640 /opt/vole-runner/runner.env /etc/vole/runner.env
111 systemctl daemon-reload
112 systemctl enable vole-runner
113
114 if [ -s /etc/vole/runner_known_hosts ]; then
115 systemctl restart vole-runner
116 else
117 echo "$FORGE_HOST did not answer yet; once it does, run: systemctl restart vole-runner"
118 fi
119
120 runcmd:
121 - /usr/local/sbin/vole-runner-first-boot
122
123 final_message: >-
124 The runner is configured. Register its key as a bot account and allow it on a
125 repository; the public key is at ${runner_dir}/.ssh/id_ed25519.pub, and setup
126 output is in /var/log/vole-runner-first-boot.log.