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/terraform/hetzner/main.tf +84

@@ -41,6 +41,22 @@ locals {
41 41 vole_package_url = var.vole_package_url
42 42 }
43 43 )
44
45 # A runner is a second machine with its own job: it leases work from the
46 # instance, runs each job in a container, and reports the result. It does not
47 # need a name of its own, an address that stays put, or backups.
48 runner_user_data = templatefile(
49 var.runner_cloud_init_file != "" ? var.runner_cloud_init_file : "${path.module}/../../cloud-init/runner.yaml",
50 {
51 runner_hostname = "${var.name}-runner"
52 forge_host = var.domain
53 runner_runtime = "podman"
54 runner_network = var.runner_network
55 runner_dir = "/var/lib/vole-runner"
56 runner_labels = join(" ", [for label in var.runner_labels : "--label ${label}"])
57 vole_package_url = var.vole_package_url
58 }
59 )
44 60 }
45 61
46 62 resource "hcloud_ssh_key" "admin" {
@@ -138,3 +154,71 @@ resource "hcloud_zone_rrset" "forge" {
138 154 type = "A"
139 155 records = [{ value = hcloud_server.vole.ipv4_address }]
140 156 }
157
158 resource "hcloud_firewall" "runner" {
159 count = var.runner ? 1 : 0
160
161 name = "${var.name}-runner"
162 labels = local.labels
163
164 # Nothing arrives except the operator's own shell. The runner dials out to the
165 # instance and to the registries its images come from.
166 rule {
167 direction = "in"
168 protocol = "tcp"
169 port = "22"
170 source_ips = var.ssh_admin_cidrs
171 }
172
173 rule {
174 direction = "in"
175 protocol = "icmp"
176 source_ips = ["0.0.0.0/0", "::/0"]
177 }
178
179 rule {
180 direction = "out"
181 protocol = "tcp"
182 port = "1-65535"
183 destination_ips = ["0.0.0.0/0", "::/0"]
184 }
185
186 rule {
187 direction = "out"
188 protocol = "udp"
189 port = "1-65535"
190 destination_ips = ["0.0.0.0/0", "::/0"]
191 }
192
193 rule {
194 direction = "out"
195 protocol = "icmp"
196 destination_ips = ["0.0.0.0/0", "::/0"]
197 }
198 }
199
200 resource "hcloud_server" "runner" {
201 count = var.runner ? 1 : 0
202
203 name = "${var.name}-runner"
204 image = var.image
205 server_type = var.runner_type
206 location = var.location
207 ssh_keys = [hcloud_ssh_key.admin.id]
208 firewall_ids = [hcloud_firewall.runner[0].id]
209 user_data = local.runner_user_data
210 labels = local.labels
211
212 # A runner holds no data worth a snapshot: its checkouts and caches are copies
213 # of what the repositories have.
214 backups = false
215
216 public_net {
217 ipv4_enabled = true
218 ipv6_enabled = true
219 }
220
221 lifecycle {
222 ignore_changes = [user_data]
223 }
224 }