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 | } | ||