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/render.sh +81−49
| @@ -1,67 +1,99 @@ | |||
| 1 | 1 | #!/bin/sh | |
| 2 | # Render deploy/cloud-init/vole.yaml for one instance, for the terminals that do | ||
| 3 | # not use Terraform. | ||
| 2 | # Render a first-boot file for one machine, for the terminals that do not use | ||
| 3 | # Terraform. | ||
| 4 | 4 | # | |
| 5 | # Usage: | ||
| 5 | # ./render.sh > user-data.yaml # the forge | ||
| 6 | # ./render.sh runner > runner-user-data.yaml # a CI runner | ||
| 6 | 7 | # | |
| 7 | # HOSTNAME=forge.example.com \ | ||
| 8 | # ADMIN_ACCOUNT=you \ | ||
| 9 | # ADMIN_EMAIL=you@example.com \ | ||
| 10 | # ADMIN_SSH_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ | ||
| 11 | # VOLE_PACKAGE_URL=https://example.com/r/vole/releases/v0.2.0/artifacts/vole_0.2.0-1_amd64.deb \ | ||
| 12 | # ./render.sh > user-data.yaml | ||
| 13 | # | ||
| 14 | # Unset values fall back to the defaults below. INSTANCE_NAME defaults to "Vole" | ||
| 15 | # and TLS_MODE to "acme"; set TLS_MODE=proxy to leave TLS to a proxy you run. | ||
| 16 | # | ||
| 17 | # The output is what a cloud provider's "user data" field wants. See | ||
| 18 | # docs/deploy/vps.md for providers that accept it through their web console. | ||
| 8 | # Values come from the environment. Anything unset falls back to the default | ||
| 9 | # below. The output goes into a cloud provider's "user data" field. See | ||
| 10 | # docs/deploy/vps.md and docs/deploy/runner.md. | ||
| 19 | 11 | set -eu | |
| 20 | 12 | ||
| 21 | 13 | here="$(cd "$(dirname "$0")" && pwd)" | |
| 14 | what="${1:-vole}" | ||
| 15 | |||
| 16 | if ! command -v envsubst >/dev/null 2>&1; then | ||
| 17 | echo "envsubst is missing; install gettext (Debian and Ubuntu: apt install gettext-base)" >&2 | ||
| 18 | exit 2 | ||
| 19 | fi | ||
| 20 | |||
| 21 | # render <template> <placeholder names...> | ||
| 22 | # | ||
| 23 | # Only the named placeholders are substituted. A name outside the list is a typo, | ||
| 24 | # and envsubst would leave it in the output as literal text, so the list is the | ||
| 25 | # contract between this script and the template. | ||
| 26 | render() { | ||
| 27 | template="$1" | ||
| 28 | shift | ||
| 29 | list="" | ||
| 30 | for name in "$@"; do | ||
| 31 | list="$list\${$name} " | ||
| 32 | done | ||
| 33 | rendered="$(envsubst "$list" <"$here/$template")" | ||
| 34 | if printf '%s\n' "$rendered" | grep -q '\${'; then | ||
| 35 | echo "unsubstituted placeholder in $template:" >&2 | ||
| 36 | printf '%s\n' "$rendered" | grep -n '\${' >&2 | ||
| 37 | exit 1 | ||
| 38 | fi | ||
| 39 | printf '%s\n' "$rendered" | ||
| 40 | } | ||
| 22 | 41 | ||
| 23 | : "${HOSTNAME:?set HOSTNAME to the DNS name the instance will answer on}" | ||
| 24 | : "${ADMIN_ACCOUNT:?set ADMIN_ACCOUNT to the account name to create}" | ||
| 25 | : "${ADMIN_SSH_KEY:?set ADMIN_SSH_KEY to the public key of that account}" | ||
| 26 | : "${VOLE_PACKAGE_URL:?set VOLE_PACKAGE_URL to the .deb for this machine's architecture}" | ||
| 27 | : "${ADMIN_EMAIL:=}" | ||
| 28 | : "${INSTANCE_NAME:=Vole}" | ||
| 29 | : "${TLS_MODE:=acme}" | ||
| 42 | case "$what" in | ||
| 43 | vole) | ||
| 44 | : "${HOSTNAME:?set HOSTNAME to the DNS name the instance will answer on}" | ||
| 45 | : "${ADMIN_ACCOUNT:?set ADMIN_ACCOUNT to the account name to create}" | ||
| 46 | : "${ADMIN_SSH_KEY:?set ADMIN_SSH_KEY to the public key of that account}" | ||
| 47 | : "${VOLE_PACKAGE_URL:?set VOLE_PACKAGE_URL to the .deb for this machine's architecture}" | ||
| 48 | : "${ADMIN_EMAIL:=}" | ||
| 49 | : "${INSTANCE_NAME:=Vole}" | ||
| 50 | : "${TLS_MODE:=acme}" | ||
| 30 | 51 | ||
| 31 | case "$TLS_MODE" in | ||
| 52 | case "$TLS_MODE" in | ||
| 32 | 53 | acme | proxy) ;; | |
| 33 | 54 | *) | |
| 34 | 55 | echo "TLS_MODE must be acme or proxy, got $TLS_MODE" >&2 | |
| 35 | 56 | exit 2 | |
| 36 | 57 | ;; | |
| 37 | esac | ||
| 58 | esac | ||
| 38 | 59 | ||
| 39 | if ! command -v envsubst >/dev/null 2>&1; then | ||
| 40 | echo "envsubst is missing; install gettext (Debian and Ubuntu: apt install gettext-base)" >&2 | ||
| 41 | exit 2 | ||
| 42 | fi | ||
| 60 | export hostname="$HOSTNAME" | ||
| 61 | export admin_account="$ADMIN_ACCOUNT" | ||
| 62 | export admin_ssh_key="$ADMIN_SSH_KEY" | ||
| 63 | export admin_email="$ADMIN_EMAIL" | ||
| 64 | export instance_name="$INSTANCE_NAME" | ||
| 65 | export tls_mode="$TLS_MODE" | ||
| 66 | export vole_package_url="$VOLE_PACKAGE_URL" | ||
| 43 | 67 | ||
| 44 | # Only the placeholders are substituted. A name outside this list is a typo, and | ||
| 45 | # envsubst would silently leave it in the output as literal text, so the list is | ||
| 46 | # the contract between this script and the template. | ||
| 47 | export hostname="$HOSTNAME" | ||
| 48 | export admin_account="$ADMIN_ACCOUNT" | ||
| 49 | export admin_ssh_key="$ADMIN_SSH_KEY" | ||
| 50 | export admin_email="$ADMIN_EMAIL" | ||
| 51 | export instance_name="$INSTANCE_NAME" | ||
| 52 | export tls_mode="$TLS_MODE" | ||
| 53 | export vole_package_url="$VOLE_PACKAGE_URL" | ||
| 68 | render vole.yaml hostname admin_account admin_ssh_key admin_email instance_name tls_mode vole_package_url | ||
| 69 | ;; | ||
| 70 | runner) | ||
| 71 | : "${FORGE_HOST:?set FORGE_HOST to the instance's DNS name}" | ||
| 72 | : "${VOLE_PACKAGE_URL:?set VOLE_PACKAGE_URL to the .deb for this machine's architecture}" | ||
| 73 | : "${RUNNER_HOSTNAME:=vole-runner}" | ||
| 74 | : "${RUNNER_RUNTIME:=podman}" | ||
| 75 | : "${RUNNER_NETWORK:=bridge}" | ||
| 76 | : "${RUNNER_DIR:=/var/lib/vole-runner}" | ||
| 77 | # Labels a job's runs-on can ask for, space separated: "linux amd64". | ||
| 78 | : "${RUNNER_LABEL_NAMES:=linux}" | ||
| 54 | 79 | ||
| 55 | placeholder_list='${hostname} ${admin_account} ${admin_ssh_key} ${admin_email} ${instance_name} ${tls_mode} ${vole_package_url}' | ||
| 80 | labels="" | ||
| 81 | for name in $RUNNER_LABEL_NAMES; do | ||
| 82 | labels="$labels--label $name " | ||
| 83 | done | ||
| 56 | 84 | ||
| 57 | rendered="$(envsubst "$placeholder_list" <"$here/vole.yaml")" | ||
| 85 | export runner_hostname="$RUNNER_HOSTNAME" | ||
| 86 | export forge_host="$FORGE_HOST" | ||
| 87 | export runner_runtime="$RUNNER_RUNTIME" | ||
| 88 | export runner_network="$RUNNER_NETWORK" | ||
| 89 | export runner_dir="$RUNNER_DIR" | ||
| 90 | export runner_labels="$labels" | ||
| 91 | export vole_package_url="$VOLE_PACKAGE_URL" | ||
| 58 | 92 | ||
| 59 | # A placeholder means the caller's value did not arrive, and the instance would | ||
| 60 | # boot with the literal text in its configuration. | ||
| 61 | if printf '%s\n' "$rendered" | grep -q '\${'; then | ||
| 62 | echo "unsubstituted placeholder in the rendered file:" >&2 | ||
| 63 | printf '%s\n' "$rendered" | grep -n '\${' >&2 | ||
| 64 | exit 1 | ||
| 65 | fi | ||
| 66 | |||
| 67 | printf '%s\n' "$rendered" | ||
| 93 | render runner.yaml runner_hostname forge_host runner_runtime runner_network runner_dir runner_labels vole_package_url | ||
| 94 | ;; | ||
| 95 | *) | ||
| 96 | echo "unknown template $what; use vole or runner" >&2 | ||
| 97 | exit 2 | ||
| 98 | ;; | ||
| 99 | esac | ||