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