Files
proxmox-scripts/install/runner-install.sh
T

193 lines
8.5 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 16128 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; }
run_user() { runuser -u "$APP_USER" -- env HOME="$APP_HOME" "$@"; }
# ── Pakete: git/rsync, Node (checkout-Action), Docker (Test-Container) ────────
setup_base_apt git rsync ca-certificates curl
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).
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; }
# act_runner schreibt .runner ins CWD des Users — sicherstellen, dass es im RUNNER_DIR liegt
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"