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/digitalocean/main.tf +77

@@ -39,6 +39,22 @@ locals {
39 39 vole_package_url = var.vole_package_url
40 40 }
41 41 )
42
43 # A runner is a second machine with its own job: it leases work from the
44 # instance, runs each job in a container, and reports the result. It does not
45 # need a name of its own, an address that stays put, or backups.
46 runner_user_data = templatefile(
47 var.runner_cloud_init_file != "" ? var.runner_cloud_init_file : "${path.module}/../../cloud-init/runner.yaml",
48 {
49 runner_hostname = "${var.name}-runner"
50 forge_host = var.domain
51 runner_runtime = "podman"
52 runner_network = var.runner_network
53 runner_dir = "/var/lib/vole-runner"
54 runner_labels = join(" ", [for label in var.runner_labels : "--label ${label}"])
55 vole_package_url = var.vole_package_url
56 }
57 )
42 58 }
43 59
44 60 resource "digitalocean_ssh_key" "admin" {
@@ -138,3 +154,64 @@ resource "digitalocean_record" "forge" {
138 154 value = digitalocean_droplet.vole.ipv4_address
139 155 ttl = 300
140 156 }
157
158 resource "digitalocean_firewall" "runner" {
159 count = var.runner ? 1 : 0
160
161 name = "${var.name}-runner"
162 droplet_ids = [digitalocean_droplet.runner[0].id]
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 inbound_rule {
167 protocol = "tcp"
168 port_range = "22"
169 source_addresses = var.ssh_admin_cidrs
170 }
171
172 inbound_rule {
173 protocol = "icmp"
174 source_addresses = ["0.0.0.0/0", "::/0"]
175 }
176
177 outbound_rule {
178 protocol = "tcp"
179 port_range = "1-65535"
180 destination_addresses = ["0.0.0.0/0", "::/0"]
181 }
182
183 outbound_rule {
184 protocol = "udp"
185 port_range = "1-65535"
186 destination_addresses = ["0.0.0.0/0", "::/0"]
187 }
188
189 outbound_rule {
190 protocol = "icmp"
191 destination_addresses = ["0.0.0.0/0", "::/0"]
192 }
193 }
194
195 resource "digitalocean_droplet" "runner" {
196 count = var.runner ? 1 : 0
197
198 name = "${var.name}-runner"
199 image = var.image
200 size = var.runner_type
201 region = var.region
202 ssh_keys = [digitalocean_ssh_key.admin.fingerprint]
203 tags = local.labels
204 ipv6 = true
205
206 user_data = local.runner_user_data
207
208 monitoring = true
209
210 # A runner holds no data worth a backup: its checkouts and caches are copies of
211 # what the repositories have.
212 backups = false
213
214 lifecycle {
215 ignore_changes = [user_data]
216 }
217 }