From 2a9d3123266b5e10b4dab7e78ad3c3ef250a0825 Mon Sep 17 00:00:00 2001 From: swaphb Date: Tue, 11 Aug 2026 16:26:09 -0400 Subject: [PATCH] add Flatcar Proxmox Gitea runner bootstrap --- scripts/proxmox/README.md | 47 ++++ .../proxmox/create-flatcar-gitea-runner.sh | 220 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 scripts/proxmox/README.md create mode 100755 scripts/proxmox/create-flatcar-gitea-runner.sh diff --git a/scripts/proxmox/README.md b/scripts/proxmox/README.md new file mode 100644 index 0000000..e433c3f --- /dev/null +++ b/scripts/proxmox/README.md @@ -0,0 +1,47 @@ +# Proxmox Flatcar Gitea runner + +`create-flatcar-gitea-runner.sh` creates a Flatcar VM using the Stable Proxmox +image and provisions it with Ignition through Proxmox `cicustom` user-data. +The VM uses 4 CPU cores, 16 GiB memory, and a 100 GiB disk by default. It +enables Docker and starts the Gitea Actions runner in a persistent container. + +The script follows the Flatcar Proxmox flow: import the image, attach a +Cloud-Init drive, place Ignition JSON in the Proxmox snippets directory, and +set `qm set --cicustom user=...`. Flatcar uses the same `user-data` path for +Ignition and regular cloud-init, so do not combine this snippet with regular +cloud-init settings. + +Before running it: + +1. Enable the `snippets` content type on the Proxmox storage used by + `SNIPPET_STORAGE`. +2. Obtain a repository, organization, or instance Gitea runner registration + token. +3. Confirm the VM ID is unused and the bridge and storage names are correct. + +Example: + +```bash +chmod +x scripts/proxmox/create-flatcar-gitea-runner.sh + +VM_ID=9100 \ +GITEA_INSTANCE_URL=https://git.example.com \ +GITEA_RUNNER_REGISTRATION_TOKEN='replace-with-token' \ +SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ +scripts/proxmox/create-flatcar-gitea-runner.sh +``` + +The registration token is written to a root-only file inside the VM. It is +also present in the generated Proxmox snippet, so protect the snippet storage +and remove it after the VM is provisioned if recovery from the snippet is not +required. + +The runner mounts the Docker socket because Gitea Actions uses Docker to +execute workflow jobs. A Docker socket is equivalent to broad host control, +so this VM should be dedicated to trusted CI workloads and should not host +unrelated services. + +The image URL and optional SHA256 value can be overridden through environment +variables. The default URL tracks the Flatcar Stable current amd64 image. Pin +`FLATCAR_IMAGE_URL` and set `FLATCAR_IMAGE_SHA256` for a reproducible change +controlled deployment. diff --git a/scripts/proxmox/create-flatcar-gitea-runner.sh b/scripts/proxmox/create-flatcar-gitea-runner.sh new file mode 100755 index 0000000..5cf79f2 --- /dev/null +++ b/scripts/proxmox/create-flatcar-gitea-runner.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# Create a Flatcar VM on Proxmox and bootstrap a Docker-based Gitea Actions runner. +# Run this on the Proxmox host as root. +set -Eeuo pipefail + +usage() { + cat <<'EOF' +Usage: + VM_ID=9100 \ + GITEA_INSTANCE_URL=https://gitea.example.com \ + GITEA_RUNNER_REGISTRATION_TOKEN='token' \ + SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)" \ + ./create-flatcar-gitea-runner.sh + +Required environment variables: + VM_ID Proxmox VM ID that does not already exist + GITEA_INSTANCE_URL Gitea URL, including scheme + GITEA_RUNNER_REGISTRATION_TOKEN Repository, organization, or instance token + SSH_PUBLIC_KEY SSH public key for the Flatcar core user + +Optional environment variables: + VM_NAME VM name, default flatcar-gitea-runner + CORES CPU cores, default 4 + MEMORY_MB Memory in MiB, default 16384 + DISK_SIZE Final disk size, default 100G + BRIDGE Proxmox bridge, default vmbr0 + DISK_STORAGE Storage for the VM disk, default local-lvm + CLOUDINIT_STORAGE Storage for the Cloud-Init drive, default local-lvm + SNIPPET_STORAGE Proxmox storage containing snippets, default local + SNIPPET_DIR Host snippet directory, default /var/lib/vz/snippets + FLATCAR_IMAGE_URL Flatcar image URL, default Stable amd64 current image + FLATCAR_IMAGE_SHA256 Optional expected SHA256 for the downloaded image + IMAGE_CACHE_DIR Host image cache, default /var/lib/vz/template/cache + GITEA_RUNNER_NAME Runner name, default flatcar-gitea-runner-VM_ID + GITEA_RUNNER_LABELS Runner labels, default ubuntu-latest:docker://node:20-bookworm + GITEA_RUNNER_IMAGE Runner image, default docker.io/gitea/act_runner:latest + +The script uses DHCP for the VM network. The generated VM receives an Ignition +configuration through Proxmox's user-data snippet mechanism. Do not reuse the +snippet file or VM disk without removing the registration token first. +EOF +} + +die() { + echo "ERROR: $*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +[[ "${1:-}" != "--help" && "${1:-}" != "-h" ]] || { usage; exit 0; } + +[[ "$(id -u)" -eq 0 ]] || die "run this script as root on the Proxmox host" + +for command_name in qm pvesm curl sha256sum jq; do + require_command "$command_name" +done + +: "${VM_ID:?VM_ID is required}" +: "${GITEA_INSTANCE_URL:?GITEA_INSTANCE_URL is required}" +: "${GITEA_RUNNER_REGISTRATION_TOKEN:?GITEA_RUNNER_REGISTRATION_TOKEN is required}" +: "${SSH_PUBLIC_KEY:?SSH_PUBLIC_KEY is required}" + +VM_NAME="${VM_NAME:-flatcar-gitea-runner}" +CORES="${CORES:-4}" +MEMORY_MB="${MEMORY_MB:-16384}" +DISK_SIZE="${DISK_SIZE:-100G}" +BRIDGE="${BRIDGE:-vmbr0}" +DISK_STORAGE="${DISK_STORAGE:-local-lvm}" +CLOUDINIT_STORAGE="${CLOUDINIT_STORAGE:-local-lvm}" +SNIPPET_STORAGE="${SNIPPET_STORAGE:-local}" +SNIPPET_DIR="${SNIPPET_DIR:-/var/lib/vz/snippets}" +FLATCAR_IMAGE_URL="${FLATCAR_IMAGE_URL:-https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_proxmoxve_image.img}" +IMAGE_CACHE_DIR="${IMAGE_CACHE_DIR:-/var/lib/vz/template/cache}" +GITEA_RUNNER_NAME="${GITEA_RUNNER_NAME:-flatcar-gitea-runner-${VM_ID}}" +GITEA_RUNNER_LABELS="${GITEA_RUNNER_LABELS:-ubuntu-latest:docker://node:20-bookworm}" +GITEA_RUNNER_IMAGE="${GITEA_RUNNER_IMAGE:-docker.io/gitea/act_runner:latest}" + +[[ "$VM_ID" =~ ^[0-9]+$ ]] || die "VM_ID must be numeric" +[[ "$CORES" =~ ^[0-9]+$ ]] || die "CORES must be numeric" +[[ "$MEMORY_MB" =~ ^[0-9]+$ ]] || die "MEMORY_MB must be numeric" +[[ "$GITEA_INSTANCE_URL" =~ ^https?:// ]] || die "GITEA_INSTANCE_URL must begin with http:// or https://" + +if qm status "$VM_ID" >/dev/null 2>&1; then + die "VM $VM_ID already exists; choose another VM_ID" +fi + +storage_config="$(pvesm config "$SNIPPET_STORAGE" 2>/dev/null || true)" +grep -Eq '(^|,)snippets(,|$)' <<<"$storage_config" || die "storage $SNIPPET_STORAGE is not configured for snippets. Enable it with: pvesm set $SNIPPET_STORAGE --content snippets" + +mkdir -p "$SNIPPET_DIR" "$IMAGE_CACHE_DIR" +chmod 0750 "$SNIPPET_DIR" + +work_dir="$(mktemp -d /tmp/flatcar-runner.XXXXXX)" +cleanup() { + rm -rf "$work_dir" +} +trap cleanup EXIT + +image_path="$IMAGE_CACHE_DIR/$(basename "$FLATCAR_IMAGE_URL")" +snippet_name="flatcar-gitea-runner-${VM_ID}-user-data" +snippet_path="$SNIPPET_DIR/$snippet_name" + +if [[ -s "$image_path" ]]; then + echo "Using cached Flatcar image: $image_path" +else + echo "Downloading Flatcar image: $FLATCAR_IMAGE_URL" + curl --fail --location --retry 3 --output "$image_path.partial" "$FLATCAR_IMAGE_URL" + mv "$image_path.partial" "$image_path" +fi + +if [[ -n "${FLATCAR_IMAGE_SHA256:-}" ]]; then + echo "Checking Flatcar image SHA256" + printf '%s %s\n' "$FLATCAR_IMAGE_SHA256" "$image_path" | sha256sum --check --strict - +fi + +runner_env="$work_dir/runner.env" +cat >"$runner_env" <"$runner_unit" <"$snippet_path" + +chmod 0600 "$snippet_path" + +echo "Creating Proxmox VM $VM_ID" +qm create "$VM_ID" \ + --name "$VM_NAME" \ + --ostype l26 \ + --cores "$CORES" \ + --memory "$MEMORY_MB" \ + --net0 "virtio,bridge=$BRIDGE" \ + --ipconfig0 ip=dhcp \ + --agent enabled=1 \ + --onboot 1 \ + --scsihw virtio-scsi-single + +qm disk import "$VM_ID" "$image_path" "$DISK_STORAGE" +qm set "$VM_ID" --scsi0 "$DISK_STORAGE:vm-${VM_ID}-disk-0" +qm resize "$VM_ID" scsi0 "$DISK_SIZE" +qm set "$VM_ID" --boot order=scsi0 +qm set "$VM_ID" --ide2 "$CLOUDINIT_STORAGE:cloudinit" +qm set "$VM_ID" --cicustom "user=${SNIPPET_STORAGE}:snippets/${snippet_name}" + +echo "Starting VM $VM_ID" +qm start "$VM_ID" + +cat < 'systemctl status docker gitea-runner --no-pager' + ssh core@ 'docker ps' + +Security note: the runner mounts /var/run/docker.sock so workflow jobs can +launch Docker containers. Treat this VM as a dedicated CI trust boundary. +EOF