Files
proxmox-scripts/install/runner-install.sh
T
claude-bot 8c83fb372b
CI / Shell-Lint (bash -n, source-check, Validierungs-Tests) (pull_request) Successful in 2s
feat(runner): install the build toolchain compiled languages need
The runner is host-mode, so there is no image bringing tools along: what is
not on this LXC, no job has. Measured on l.kirchner/patchmgr, a Go project,
where all six CI jobs were assigned and every one of them died in the first
seconds:

    make all         make: command not found
    go test -race    go: -race requires cgo; enable cgo by setting CGO_ENABLED=1
    make proto       sudo: command not found

Four packages, each for a reason:

    make               the gate commands are make targets
    gcc                Go turns CGO_ENABLED off when it finds no C compiler,
                       and the race detector cannot be built without cgo
    protobuf-compiler  protoc itself
    libprotobuf-dev    the well-known .proto includes under
                       /usr/include/google/protobuf; without them protoc fails
                       even though the binary is there

sudo stays absent on purpose. A workflow must not be able to install anything
on this runner -- what is needed is declared here, in the script, and not in
somebody's pipeline. That also keeps the security note at the top of this file
honest: the LXC owns nothing, and it gains nothing at a workflow's request.

Go is not in the list. Projects fetch it through actions/setup-go, because CI
matrices run more than one version.

Applied to the running LXC (301 on pve-gamer) while the runner was idle, then
verified: make 4.3, gcc 12.2.0, libprotoc 3.21.12, 11 .proto includes present.
The service PATH already contains /usr/bin, so no restart was needed.
2026-08-18 22:54:54 +02:00

215 lines
9.8 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Runner installer — runs inside the LXC, called by ct/runner.sh
#
# Allgemeiner, INSTANZWEITER Gitea-Actions-Runner ohne Deploy-Rechte:
# - Node.js (für actions/checkout im Host-Mode) + git + rsync
# - Docker (für Wegwerf-Test-Container in CI, z. B. pgvector)
# - act_runner als unprivilegierter User "runner" in der docker-Gruppe
# - systemd-Unit act-runner.service (Requires=docker.service)
#
# SICHERHEITSMODELL (Review-Finding 88, bewusst akzeptiert & dokumentiert):
# docker-Gruppe == de-facto root IN DIESEM LXC. Da der Runner instanzweit
# ist, kann jedes Repo der Gitea-Instanz via Workflow den Runner-Container
# vollständig kontrollieren. Das ist hier akzeptiert, weil (a) alle Repos
# der Instanz vom selben Admin (Lutz) stammen — kein Multi-Tenant — und
# (b) dieser LXC GENAU DESHALB nichts besitzt: keine sudoers, keine
# Produktions-Mounts, keine Secrets außer dem (geshredderten) Reg-Token.
# Bei Öffnung der Instanz für Dritte: Docker rootless oder eigener Runner
# pro Vertrauenszone. NICHT auf LXCs mit Deploy-Rechten übertragen.
#
# Idempotent: erneuter Lauf (auch OHNE /root/runner.deploy.env) überspringt
# Vorhandenes und provisioniert nur nach — Registrierung braucht die env-Datei
# nur beim Erstlauf (Finding 90).
set -euo pipefail
LIB_URL="${LIB_URL:-https://gitea.luki-net.org/luki-net/proxmox-scripts/raw/branch/main/lib}"
source <(curl -fsSL "$LIB_URL/install.func")
[[ "$EUID" -eq 0 ]] || { msg_err "Must run as root"; exit 1; }
APP_USER="runner"
APP_HOME="/opt/runner"
RUNNER_DIR="$APP_HOME/data"
# ── Konfiguration laden (optional bei Re-Run, Finding 90) ─────────────────────
CONF="/root/runner.deploy.env"
if [[ -f "$CONF" ]]; then
set -a; . "$CONF"; set +a
fi
RUNNER_NAME="${RUNNER_NAME:-$(hostname)}"
RUNNER_LABELS="${RUNNER_LABELS:-homelab:host}"
RUNNER_VERSION="${RUNNER_VERSION:-0.2.13}"
NODE_MAJOR="${NODE_MAJOR:-22}"
# ── Validierung (Finding 80): Werte landen in Shell-Fragmenten/systemd —
# strikte Zeichenklassen statt Vertrauen. Re-Runs ohne CONF validieren
# nur, was gesetzt ist; Registrierungs-Pflichtwerte prüft der Reg-Block. ──
valid_url() { [[ "$1" =~ ^https?://[A-Za-z0-9.-]+(:[0-9]{1,5})?$ ]]; }
valid_token() { [[ "$1" =~ ^[A-Za-z0-9_-]{16,128}$ ]]; }
valid_token_word() { [[ "$1" =~ ^[A-Za-z0-9._:,-]+$ ]]; }
[[ -z "${GITEA_INSTANCE_URL:-}" ]] || valid_url "$GITEA_INSTANCE_URL" \
|| { msg_err "GITEA_INSTANCE_URL ungültig: nur http(s)://host[:port]"; exit 1; }
[[ -z "${RUNNER_TOKEN:-}" ]] || valid_token "$RUNNER_TOKEN" \
|| { msg_err "RUNNER_TOKEN ungültig (erwartet 16–128 Zeichen [A-Za-z0-9_-])"; exit 1; }
valid_token_word "$RUNNER_NAME" || { msg_err "RUNNER_NAME enthält unzulässige Zeichen"; exit 1; }
valid_token_word "$RUNNER_LABELS" || { msg_err "RUNNER_LABELS enthält unzulässige Zeichen"; exit 1; }
[[ "$RUNNER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { msg_err "RUNNER_VERSION ungültig"; exit 1; }
[[ "$NODE_MAJOR" =~ ^[0-9]+$ ]] || { msg_err "NODE_MAJOR ungültig"; exit 1; }
# Finding 85 (Re-Review): act_runner schreibt .runner ins CWD — run_user wechselt
# deshalb hart ins RUNNER_DIR (env -C), sonst landet die Registrierung in / und
# der unprivilegierte User darf dort nicht schreiben.
run_user() { runuser -u "$APP_USER" -- env -C "$RUNNER_DIR" HOME="$APP_HOME" "$@"; }
# ── Pakete: git/rsync, Node (checkout-Action), Docker (Test-Container),
# Build-Werkzeuge (Compiler-Sprachen in CI) ─────────────────────────────────
#
# make/gcc/protobuf: Der Runner faehrt Host-Mode, also gibt es kein Image, das
# Werkzeuge mitbringt — was hier nicht liegt, hat kein Job. Konkret gemessen an
# l.kirchner/patchmgr (Go):
# make "make: command not found" in jedem Gate-Job
# gcc ohne C-Compiler setzt Go CGO_ENABLED=0, und
# "go test -race" ist dann gar nicht baubar
# protobuf-compiler protoc fuer die Codegenerierung
# libprotobuf-dev liefert /usr/include/google/protobuf/*.proto; ohne die
# Includes scheitert protoc trotz vorhandenem Binary
#
# Bewusst NICHT installiert: sudo. Ein Workflow soll auf diesem Runner nichts
# nachinstallieren koennen — was gebraucht wird, steht hier.
#
# Go selbst gehoert nicht hierher: Projekte holen es ueber actions/setup-go,
# weil CI-Matrizen mehrere Fassungen fahren.
setup_base_apt git rsync ca-certificates curl \
make gcc protobuf-compiler libprotobuf-dev
NODE_HAVE="$(command -v node >/dev/null 2>&1 && node -v | sed -E 's/^v([0-9]+).*/\1/' || echo 0)"
if [[ "$NODE_HAVE" != "$NODE_MAJOR" ]]; then
msg_info "Installing Node.js ${NODE_MAJOR}.x (NodeSource)..."
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - >/dev/null
apt-get install -y -qq nodejs >/dev/null
msg_ok "Node $(node -v) installed"
else
msg_warn "Node $(node -v) already present, skipping"
fi
if ! command -v docker >/dev/null 2>&1; then
msg_info "Installing Docker (get.docker.com)..."
curl -fsSL https://get.docker.com | sh >/dev/null
msg_ok "Docker $(docker --version | awk '{print $3}' | tr -d ',')"
else
msg_warn "Docker already present, skipping"
fi
# ── act_runner binary ─────────────────────────────────────────────────────────
if [[ ! -x /usr/local/bin/act_runner ]]; then
ARCH="$(dpkg --print-architecture)"
case "$ARCH" in amd64|arm64) ;; *) msg_err "unsupported arch: $ARCH"; exit 1 ;; esac
msg_info "Downloading act_runner $RUNNER_VERSION ($ARCH)..."
curl -fsSL "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${ARCH}" \
-o /usr/local/bin/act_runner
chmod +x /usr/local/bin/act_runner
msg_ok "act_runner $(/usr/local/bin/act_runner --version 2>/dev/null | head -n1)"
else
msg_warn "act_runner already present, skipping download"
fi
# ── User (docker-Gruppe VOR dem Daemon-Start — Lesson aus nexus K-103:
# Gruppenmitgliedschaften werden beim Prozessstart eingefroren) ─────────────
create_system_user "$APP_USER" "$APP_HOME"
usermod -aG docker "$APP_USER"
mkdir -p "$RUNNER_DIR"
chown -R "$APP_USER:$APP_USER" "$APP_HOME"
# ── Registrierung (idempotent; braucht CONF-Werte nur beim Erstlauf) ──────────
if [[ ! -f "$RUNNER_DIR/.runner" ]]; then
: "${GITEA_INSTANCE_URL:?missing GITEA_INSTANCE_URL (Erstlauf braucht /root/runner.deploy.env)}"
: "${RUNNER_TOKEN:?missing RUNNER_TOKEN (Erstlauf braucht /root/runner.deploy.env)}"
msg_info "Registering runner '$RUNNER_NAME' [$RUNNER_LABELS] with $GITEA_INSTANCE_URL (instance scope)..."
# Werte sind oben strikt validiert (keine Quotes/Whitespace möglich) —
# Übergabe als argv an runuser, keine erneute Shell-Interpolation (vgl. Finding 76).
# CWD = RUNNER_DIR via run_user (Finding 85).
run_user /usr/local/bin/act_runner register \
--no-interactive \
--config /dev/null \
--instance "$GITEA_INSTANCE_URL" \
--token "$RUNNER_TOKEN" \
--name "$RUNNER_NAME" \
--labels "$RUNNER_LABELS" \
2>&1 | sed "s#$RUNNER_TOKEN#<redacted>#g" || { msg_err "Registrierung fehlgeschlagen"; exit 1; }
# Belt-and-suspenders: falls eine künftige act_runner-Version doch ins HOME schreibt
if [[ -f "$APP_HOME/.runner" && ! -f "$RUNNER_DIR/.runner" ]]; then
mv "$APP_HOME/.runner" "$RUNNER_DIR/.runner"
fi
[[ -f "$RUNNER_DIR/.runner" ]] || { msg_err ".runner nach Registrierung nicht gefunden"; exit 1; }
chown -R "$APP_USER:$APP_USER" "$RUNNER_DIR"
msg_ok "Runner registered"
else
msg_warn "Runner already registered (.runner exists), skipping"
fi
# ── SSH-Root-Login-Policy anwenden, falls vom Host-Script übergeben
# (Funktion existiert in install.func ab K-114/PR #5 — guarded Aufruf,
# damit dieser Branch vor und nach dem Merge funktioniert) ────────────────
if declare -F configure_ssh_root_login >/dev/null 2>&1; then
configure_ssh_root_login
fi
# ── systemd-Unit (bewusst KEINE sudoers-Datei; Findings 82 + 70) ──────────────
cat >/etc/systemd/system/act-runner.service <<EOF
[Unit]
Description=Gitea Actions runner (instance-wide, label ${RUNNER_LABELS%%:*}, no deploy rights)
After=network-online.target docker.service
Wants=network-online.target
# Finding 82: ohne Docker keine Jobs annehmen — harte Kopplung
Requires=docker.service
[Service]
Type=simple
User=$APP_USER
Group=$APP_USER
WorkingDirectory=$RUNNER_DIR
Environment=HOME=$APP_HOME
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ExecStart=/usr/local/bin/act_runner daemon
Restart=on-failure
RestartSec=5
# Finding 70: Basis-Härtung (docker-CLI über Socket bleibt funktional)
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now act-runner.service
msg_ok "act-runner.service installed + started"
CRED_FILE="/root/runner.credentials"
cat >"$CRED_FILE" <<EOF
Allgemeiner Gitea-Actions-Runner (instanzweit)
Instance: ${GITEA_INSTANCE_URL:-<bestehende Registrierung, siehe $RUNNER_DIR/.runner>}
Name/Label: $RUNNER_NAME [$RUNNER_LABELS]
User: $APP_USER (docker-Gruppe, KEIN sudo)
Workdir: $RUNNER_DIR
Logs: journalctl -u act-runner -f
Repos nutzen ihn mit: runs-on: ${RUNNER_LABELS%%:*}
SICHERHEITSMODELL: docker-Gruppe == de-facto root in DIESEM LXC; instanzweit
heißt: jedes Repo der Instanz kann den Runner-LXC kontrollieren. Akzeptiert,
weil Single-Admin-Instanz und dieser LXC nichts besitzt (keine sudoers, keine
Produktions-Mounts). Deploy-Jobs gehören NICHT hierher — die bleiben auf dem
repo-scoped nexus-Runner. Trennung beibehalten.
EOF
chmod 600 "$CRED_FILE"
if [[ -f "$CONF" ]]; then
shred -u "$CONF" 2>/dev/null || rm -f "$CONF"
fi
apt_cleanup
msg_ok "runner installation finished"