diff --git a/README.md b/README.md index e865528..c712543 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ These scripts automate the deployment of personal infrastructure components. The - **Self-contained**: No external dependencies beyond standard Debian packages - **Idempotent-ish**: Safe to re-run (where possible) - **Curl-friendly**: Designed for one-liner deployment from a fresh server -- **Multi-OS**: Supports Debian and Alpine-based deployments +- **Multi-OS**: Supports Debian and Alpine-based deployments, chosen per-script based on that service's requirements - **Loopback by default**: Services bind to `127.0.0.1`; Tailscale handles the reverse proxy and TLS termination - **Log hygiene**: Every long-running service ships with a `logrotate` config (no unbounded log files) - **Console auto-login**: Proxmox LXCs are configured for root auto-login on `tty1` (fast `pct enter` and Web UI shell access) @@ -23,11 +23,10 @@ These scripts automate the deployment of personal infrastructure components. The | [`netlab/install.sh`](netlab/) | Network lab with ContainerLab | `curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/netlab/install.sh` \| `bash` | | [`gitea-runner/install.sh`](gitea-runner/) | Gitea Act Runner on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)"` | | [`openbao/install.sh`](openbao/) | OpenBao secrets manager on Alpine LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/openbao/install.sh)"` | -| [`ferretdb/install.sh`](ferretdb/) | FerretDB (MongoDB-compatible) on Debian LXC (Proxmox) | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)"` | | [`komodo/install.sh`](komodo/) | Komodo (Docker + MongoDB) on Alpine VM | `bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/komodo/install.sh)"` | ### Requirements -- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao, ferretdb) or Alpine VM (komodo) -- User with sudo privileges (do not run as root) — except gitea-runner, openbao, ferretdb, and komodo which run as root +- Fresh Debian 12/13 installation (proxy, netlab) or Proxmox VE host (gitea-runner, openbao) or Alpine VM (komodo) +- User with sudo privileges (do not run as root) — except gitea-runner, openbao, and komodo which run as root - Internet access diff --git a/ferretdb/README.md b/ferretdb/README.md deleted file mode 100644 index 12f6897..0000000 --- a/ferretdb/README.md +++ /dev/null @@ -1,139 +0,0 @@ -# FerretDB - -Automated installation and update script for [FerretDB](https://www.ferretdb.io) — a truly -open-source, MongoDB-compatible database — running inside a **Debian LXC** on Proxmox. - -A drop-in MongoDB replacement for any app that speaks the MongoDB wire protocol (e.g. -[LibreChat](https://www.librechat.ai)) without running MongoDB itself. The script installs -and exposes the database only; wiring it into an application is left to that app's own -configuration (a separate script, or manually). - -### Why Debian and not Alpine? - -FerretDB v2 is two pieces: - -1. the **FerretDB proxy** (a static Go binary), and -2. **PostgreSQL + Microsoft's DocumentDB extension**, the mandatory storage engine. - -The DocumentDB extension is a compiled C PostgreSQL extension and is published **only** as -`deb`/`rpm` packages (`deb11`, `deb12`, `ubuntu`, `rhel`) — there is **no Alpine/musl build**. -So, unlike the `openbao`/`gitea-runner` Alpine LXCs in this repo, this stack runs on Debian 12 -(`deb12`, the newest target the extension ships for). - -### Features - -Single script, automatic mode selection: - -| Context | Action | -| ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | -| From Proxmox host, no existing FerretDB container | Detects newest Debian template, creates LXC, installs PostgreSQL + DocumentDB + FerretDB | -| From Proxmox host, FerretDB container already present | Reuses the existing LXC, refreshes packages, upgrades the FerretDB stack to latest | -| From inside an LXC, no `ferretdb` binary | Installs the full stack from scratch | -| From inside an LXC, `ferretdb` already present | Updates the packages only (no config / role / data changes) | - -The container is identified by hostname **and** the `ferretdb` tag, so it is re-found across -reruns even if the CTID was auto-allocated the first time. - -### Requirements - -- Proxmox VE host with `pveam`, `pct`, `pvesh`, `jq` available -- Internet access from both the host (template download) and the LXC (package downloads) -- Script must be run as **root** on the Proxmox host (enforced; the Web UI shell qualifies) - -### Usage - -#### Full install (from Proxmox shell) - -```bash -bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)" -``` - -The script prints the generated password and the ready-to-paste `MONGO_URI` at the end. -**Save them** — the password is not persisted on the Proxmox host. - -Re-running the exact same command later upgrades packages inside the LXC and brings the -FerretDB stack to the latest release, without touching the config, role, or PostgreSQL data. - -#### Customisation - -Every parameter is exposed as an environment variable: - -| Variable | Default | Description | -| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| `CTID` | auto | Container ID (auto-allocated via `pvesh get /cluster/nextid`) | -| `FERRETDB_HOSTNAME` | `ferretdb` | LXC hostname | -| `TEMPLATE` | auto-detected | Debian template; auto-detected from `pveam available` | -| `STORAGE` | `local-lvm` | Proxmox storage for the LXC root disk | -| `TEMPLATE_STORAGE` | `local` | Storage where Debian templates live | -| `CORES` | `2` | vCPU cores | -| `RAM` | `2048` | RAM in MiB (Postgres + FerretDB) | -| `DISK` | `16` | Root disk size in GB | -| `BRIDGE` | `vmbr0` | Network bridge | -| `LXC_TAG` | `ferretdb` | Stable tag used to re-discover the container | -| `PG_VERSION` | `17` | PostgreSQL major version (from PGDG) | -| `DOCUMENTDB_TAG` | `latest` | DocumentDB release tag (couples documentdb + FerretDB versions); pin e.g. `v0.107.0-ferretdb-2.7.0` | -| `DOCUMENTDB_DISTRO` | `deb12` | Distro target in the documentdb deb filename. Escape hatch for a future `deb13` (set with `TEMPLATE`) | -| `FERRETDB_LISTEN_ADDR` | `0.0.0.0:27017` | TCP listener. Exposed on all interfaces — it is a database other hosts must reach. | -| `FERRETDB_USER` | `ferretdb` | App user — both the PostgreSQL role and the MongoDB user clients authenticate as | -| `FERRETDB_PASSWORD` | auto-generated | Auto-generated (`openssl rand -hex 24`) when unset; printed in the final summary | -| `TS_AUTHKEY` | _(unset)_ | Pre-auth key (generate at ). If unset, finish `tailscale up` manually inside the LXC. | - -```bash -CTID=220 FERRETDB_HOSTNAME=mongo RAM=4096 DISK=32 \ - bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)" -``` - -#### Connecting a client - -Any MongoDB driver or tool connects with a standard connection string (the script prints the -exact one, with the generated password, at the end of the install): - -``` -mongodb://ferretdb:@:27017/ -``` - -Append a database name to target one (e.g. `…:27017/myapp`); it is created on first write. -Wiring this into a specific application (setting its Mongo connection string, disabling any -bundled MongoDB it ships, etc.) is intentionally out of scope — do it from that app's own -config or a dedicated script. - -#### Tailscale - -The LXC joins the tailnet (`tailscale up --ssh`) so a client on another node can reach the -database over the tailnet. Unlike the `openbao` script there is **no `tailscale serve`** — the -MongoDB wire protocol is raw TCP, not HTTP, so the listener is exposed directly on -`0.0.0.0:27017` (LAN + tailnet) by design. - -If `TS_AUTHKEY` was supplied the node is brought up automatically; otherwise finish it -manually inside the LXC: - -```bash -pct enter -tailscale up --ssh --hostname ferretdb -tailscale status # prints the tailnet FQDN -``` - -> Because the listener is on `0.0.0.0`, restrict access with your tailnet ACLs and/or a host -> firewall — anyone who can route to TCP 27017 can attempt to authenticate. - -#### Update (from inside the LXC) - -```bash -curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh | bash -``` - -The script auto-detects the presence of `/usr/bin/ferretdb` and switches to update mode: -packages are upgraded (including the DocumentDB extension via -`ALTER EXTENSION documentdb UPDATE`) and the services are restarted. The config, the -PostgreSQL role, and the data are left untouched. - -### Architecture - -- **OS**: latest Debian LXC template (auto-detected), unprivileged, `nesting=1`, `/dev/net/tun` passthrough for Tailscale -- **Storage engine**: PostgreSQL `17` (PGDG) + the DocumentDB extension (`pg_documentdb`, `pg_cron`), loopback-only on `127.0.0.1:5432` -- **Proxy**: official `ferretdb` deb from `github.com/FerretDB/FerretDB`, systemd unit, listening on `0.0.0.0:27017` -- **Auth**: the app user is provisioned through `documentdb_api.create_user` (roles `clusterAdmin` + `readWriteAnyDatabase`), **not** a plain `CREATE ROLE`. DocumentDB builds the SCRAM-SHA-256 verifier with its own 28-byte salt (`documentdb.scramDefaultSaltLen`); a native PostgreSQL role would store a 16-byte salt that MongoDB clients reject (`invalid salt length of 16 in sasl step2`). FerretDB connects to PostgreSQL as the same user. -- **Network**: FerretDB exposed on `0.0.0.0:27017`; Tailscale runs in the LXC for tailnet reachability (no `serve`) -- **Config**: `/etc/postgresql/17/main/conf.d/documentdb.conf` (extension settings) and `/etc/systemd/system/ferretdb.service.d/override.conf` (`FERRETDB_POSTGRESQL_URL`, `FERRETDB_LISTEN_ADDR`) -- **Logs**: PostgreSQL via its stock `logrotate`; FerretDB via journald, capped at `SystemMaxUse=200M` -- **Version tracking**: `/opt/ferretdb_version.txt` records the installed DocumentDB tag for idempotent reruns diff --git a/ferretdb/install.sh b/ferretdb/install.sh deleted file mode 100755 index 40fbaed..0000000 --- a/ferretdb/install.sh +++ /dev/null @@ -1,676 +0,0 @@ -#!/bin/bash -# install.sh - FerretDB: LXC creation, installation & update -# Usage: -# From Proxmox host : bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh)" -# From inside LXC : bash /root/install.sh (updates packages) -# -# Single entrypoint, three automatic modes: -# 1. Proxmox host, no existing container -> create Debian LXC + install FerretDB -# 2. Proxmox host, container already present -> update packages + upgrade FerretDB -# 3. Inside an LXC -> install if missing, otherwise update -# -# FerretDB v2 is a MongoDB wire-protocol proxy backed by PostgreSQL + the -# DocumentDB extension. The extension is a compiled C PostgreSQL extension and -# is only published as deb/rpm packages (no Alpine/musl build), so this stack -# runs on Debian — unlike the openbao/gitea-runner Alpine LXCs in this repo. -# -# The package install/upgrade logic lives in a single reusable function -# (install_or_upgrade_packages) shared by both the create and update paths. - -set -euo pipefail - -# Force an always-present locale. A fresh Debian template has not generated the -# host's locale (e.g. fr_FR.UTF-8 inherited through pct exec), so apt, perl and -# apt-listchanges warn loudly about it. C.UTF-8 ships with glibc and is always -# valid; this silences the noise without installing extra locales. -export LC_ALL=C.UTF-8 -export LANG=C.UTF-8 - -# --- Config (override via environment) --- -CTID="${CTID:-}" -HOSTNAME_LXC="${FERRETDB_HOSTNAME:-ferretdb}" -TEMPLATE="${TEMPLATE:-}" # auto-detected when empty -STORAGE="${STORAGE:-local-lvm}" -TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" -CORES="${CORES:-2}" -RAM="${RAM:-2048}" # Postgres needs more headroom than openbao -DISK="${DISK:-16}" -BRIDGE="${BRIDGE:-vmbr0}" -LXC_TAG="${LXC_TAG:-ferretdb}" # stable identifier for the container -PG_VERSION="${PG_VERSION:-17}" # PostgreSQL major version (PGDG) -# DocumentDB release tag couples both pieces: it encodes the documentdb package -# version AND the matching FerretDB version. "latest" resolves both at once. -DOCUMENTDB_TAG="${DOCUMENTDB_TAG:-latest}" -# Distro target embedded in the documentdb deb filename. Only deb11/deb12 exist -# today (no deb13). Escape hatch: when upstream ships deb13, set this + TEMPLATE -# to move to trixie without editing the script. -DOCUMENTDB_DISTRO="${DOCUMENTDB_DISTRO:-deb12}" -DOCUMENTDB_RELEASES_URL="${DOCUMENTDB_RELEASES_URL:-https://api.github.com/repos/FerretDB/documentdb/releases}" -# As a database we deliberately expose the listener on all interfaces so other -# hosts can reach it over the LAN / tailnet. PostgreSQL stays local. -FERRETDB_LISTEN_ADDR="${FERRETDB_LISTEN_ADDR:-0.0.0.0:27017}" -# Application credentials. The same user/password is both the PostgreSQL role -# FerretDB connects with AND the MongoDB user clients authenticate as. -FERRETDB_USER="${FERRETDB_USER:-ferretdb}" -FERRETDB_PASSWORD="${FERRETDB_PASSWORD:-}" # auto-generated when empty -# Optional: pre-authorise the LXC's Tailscale non-interactively. -# Generate at https://login.tailscale.com/admin/settings/keys -TS_AUTHKEY="${TS_AUTHKEY:-}" -# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when -# testing from a non-main branch. -SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/ferretdb/install.sh}" -VERSION_FILE="${VERSION_FILE:-/opt/ferretdb_version.txt}" - -# --- Colors --- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Logs go to stderr so callers can safely use $(fn) without capturing log noise. -log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; } -log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } - -# ============================================================ -# Generic helpers -# ============================================================ -require_root() { - if [[ "$(id -u)" -ne 0 ]]; then - log_error "This script must be run as root (current uid: $(id -u))." - log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root." - exit 1 - fi -} - -# Debian packages use dpkg-style arch names (amd64, arm64), which is also what -# both the FerretDB and DocumentDB release assets are named with. -get_arch() { - case "$(uname -m)" in - x86_64) echo "amd64" ;; - aarch64) echo "arm64" ;; - *) log_error "Unsupported architecture: $(uname -m)"; exit 1 ;; - esac -} - -# Resolve the DocumentDB release tag into the two coupled versions it encodes. -# Sets globals: DOC_TAG (full tag), DOC_PKG_VER (documentdb pkg version), -# FERRET_VER (matching FerretDB version, no leading v). -# Tag shape: v0.107.0-ferretdb-2.7.0 -resolve_versions() { - local endpoint tag - if [[ "$DOCUMENTDB_TAG" == "latest" ]]; then - endpoint="${DOCUMENTDB_RELEASES_URL}/latest" - else - endpoint="${DOCUMENTDB_RELEASES_URL}/tags/${DOCUMENTDB_TAG}" - fi - tag=$(curl -fsSL "$endpoint" | jq -r '.tag_name') - if [[ -z "$tag" || "$tag" == "null" ]]; then - log_error "Failed to resolve DocumentDB release '${DOCUMENTDB_TAG}' from GitHub API." - exit 1 - fi - DOC_TAG="$tag" - DOC_PKG_VER="${tag#v}" # 0.107.0-ferretdb-2.7.0 - DOC_PKG_VER="${DOC_PKG_VER%%-ferretdb-*}" # 0.107.0 - FERRET_VER="${tag##*-ferretdb-}" # 2.7.0 - if [[ -z "$DOC_PKG_VER" || -z "$FERRET_VER" || "$FERRET_VER" == "$tag" ]]; then - log_error "Could not parse DocumentDB tag '${tag}' (expected vX-ferretdb-Y)." - exit 1 - fi -} - -# ============================================================ -# Reusable: install or upgrade PostgreSQL + DocumentDB + FerretDB. -# Used by both fresh-install and update flows. Idempotent. -# ============================================================ -install_or_upgrade_packages() { - local arch current doc_url ferret_url tmpdir doc_deb_name doc_full_ver - resolve_versions - arch=$(get_arch) - - current="" - if [[ -f "$VERSION_FILE" ]]; then - current=$(cat "$VERSION_FILE") - fi - - if [[ "$current" == "$DOC_TAG" && -x /usr/bin/ferretdb ]]; then - log_info "FerretDB stack already at ${DOC_TAG}, nothing to do." - return 0 - fi - - # Ensure the PGDG repo is present so the requested PostgreSQL major exists. - if [[ ! -f /etc/apt/sources.list.d/pgdg.list ]]; then - log_info "Adding the PostgreSQL APT (PGDG) repository..." - install -d -m 0755 /usr/share/keyrings - curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ - | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg - echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt $(. /etc/os-release && echo "$VERSION_CODENAME")-pgdg main" \ - > /etc/apt/sources.list.d/pgdg.list - apt-get update >/dev/null - fi - - log_info "Installing PostgreSQL ${PG_VERSION} + pg_cron..." - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - "postgresql-${PG_VERSION}" "postgresql-${PG_VERSION}-cron" >/dev/null - - # documentdb deb naming: deb12-postgresql-17-documentdb_0.107.0.ferretdb.2.7.0_amd64.deb - doc_full_ver="${DOC_PKG_VER}.ferretdb.${FERRET_VER}" - doc_deb_name="${DOCUMENTDB_DISTRO}-postgresql-${PG_VERSION}-documentdb_${doc_full_ver}_${arch}.deb" - doc_url="https://github.com/FerretDB/documentdb/releases/download/${DOC_TAG}/${doc_deb_name}" - ferret_url="https://github.com/FerretDB/FerretDB/releases/download/v${FERRET_VER}/ferretdb-${arch}-linux.deb" - - tmpdir=$(mktemp -d) - log_info "Downloading DocumentDB extension (${doc_deb_name})..." - curl -fsSL "$doc_url" -o "${tmpdir}/documentdb.deb" - log_info "Downloading FerretDB ${FERRET_VER} (${arch})..." - curl -fsSL "$ferret_url" -o "${tmpdir}/ferretdb.deb" - - log_info "Installing DocumentDB extension + FerretDB (apt resolves dependencies)..." - DEBIAN_FRONTEND=noninteractive apt-get install -y \ - "${tmpdir}/documentdb.deb" "${tmpdir}/ferretdb.deb" >/dev/null - - # If the extension is already created (update path), bring it to the new version. - if su -s /bin/sh postgres -c "psql -tAc \"SELECT 1 FROM pg_extension WHERE extname='documentdb'\" -d postgres" 2>/dev/null | grep -q 1; then - log_info "Updating documentdb extension to ${DOC_PKG_VER}..." - su -s /bin/sh postgres -c "psql -d postgres -c 'ALTER EXTENSION documentdb UPDATE;'" >/dev/null 2>&1 || \ - log_warn "ALTER EXTENSION documentdb UPDATE failed — check after restart." - fi - - echo "$DOC_TAG" > "$VERSION_FILE" - rm -rf "$tmpdir" - - log_info "Installed FerretDB: $(/usr/bin/ferretdb --version 2>&1 | head -n1 || true)" - - # Restart services if they already exist (update path); the install path - # enables them explicitly after configuration. - if systemctl list-unit-files ferretdb.service >/dev/null 2>&1; then - systemctl restart postgresql 2>/dev/null || true - systemctl restart ferretdb 2>/dev/null || true - fi -} - -# ============================================================ -# Reusable: bring Tailscale up so the LXC joins the tailnet. -# Unlike openbao we do NOT use 'tailscale serve' — FerretDB speaks the raw -# MongoDB wire protocol (TCP), not HTTP, so serve does not apply. The DB is -# reachable directly on 0.0.0.0:27017 over the LAN / tailnet. -# ============================================================ -configure_tailscale() { - if ! command -v tailscale >/dev/null 2>&1; then - log_warn "tailscale CLI not found, skipping tailnet setup." - return 0 - fi - - local backend_state - backend_state=$(tailscale status --json 2>/dev/null | jq -r '.BackendState // "unknown"') - if [[ "$backend_state" == "Running" ]]; then - log_info "Tailscale already up." - return 0 - fi - - if [[ -n "$TS_AUTHKEY" ]]; then - log_info "Bringing Tailscale up with provided auth key..." - tailscale up --authkey "$TS_AUTHKEY" --ssh --hostname "$HOSTNAME_LXC" \ - || log_warn "tailscale up failed — run it manually inside the LXC." - else - log_warn "Tailscale not authenticated and TS_AUTHKEY was not supplied." - log_warn "Finish setup inside the LXC with: tailscale up --ssh --hostname ${HOSTNAME_LXC}" - fi -} - -# ============================================================ -# Proxmox-host helpers -# ============================================================ - -# The installer re-fetches itself inside the LXC from SCRIPT_URL. Verify it is -# reachable on the host *before* creating any container, so a wrong branch/path -# fails immediately with guidance instead of dying mid-install with a curl 404. -preflight_script_url() { - if curl -fsSL -o /dev/null "$SCRIPT_URL"; then - return 0 - fi - log_error "SCRIPT_URL is not reachable: ${SCRIPT_URL}" - log_error "The installer re-fetches itself inside the LXC from SCRIPT_URL, so this" - log_error "must resolve. If you are testing from a branch (not yet on main), pass it:" - log_error " SCRIPT_URL=https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch//ferretdb/install.sh \\" - log_error " bash -c \"\$(curl -fsSL \"\$SCRIPT_URL\")\"" - exit 1 -} - -# Detect newest Debian *12* LXC template available from the Proxmox repos. -# Deliberately pinned to debian-12: the DocumentDB extension only ships a deb12 -# build (DOCUMENTDB_DISTRO default), and a newer template (e.g. debian-13) would -# pair that deb against a different libicu soname. To move to trixie, set both -# TEMPLATE and DOCUMENTDB_DISTRO yourself once upstream publishes a deb13 build. -detect_latest_debian_template() { - local tmpl - tmpl=$(pveam available --section system 2>/dev/null \ - | awk '/^system[[:space:]]+debian-12-/ {print $2}' \ - | sort -V \ - | tail -n1) - - if [[ -z "$tmpl" ]]; then - log_warn "Could not find a debian-12 template via pveam; falling back to a known-good name." - tmpl="debian-12-standard_12.7-1_amd64.tar.zst" - fi - log_info "Selected Debian template: $tmpl" - echo "$tmpl" -} - -# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none. -find_existing_lxc() { - local id host tags - while read -r id _; do - [[ -z "$id" || "$id" == "VMID" ]] && continue - host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true) - tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true) - if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then - echo "$id" - return 0 - fi - done < <(pct list | awk 'NR>1 {print $1}') - return 1 -} - -ensure_template_present() { - local tmpl="$1" - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then - log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..." - pveam update >/dev/null - pveam download "$TEMPLATE_STORAGE" "$tmpl" - else - log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}." - fi -} - -# Pick next available CTID if user did not provide one. -allocate_ctid() { - pvesh get /cluster/nextid 2>/dev/null \ - || pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \ - | jq '[.[].vmid] | max + 1' \ - || echo 100 -} - -# Inject the script into the container and execute it in the requested mode. -# Forwards the relevant runtime configuration through the environment so the -# inner invocation produces the same config the user requested on the host. -exec_in_lxc() { - local ctid="$1" - local mode="$2" # --install or --update - - # Ensure base tooling exists inside the container before piping the script. - pct exec "$ctid" -- sh -c "export DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=C.UTF-8; apt-get update >/dev/null 2>&1; apt-get install -y bash curl jq ca-certificates >/dev/null 2>&1" - curl -fsSL "$SCRIPT_URL" \ - | pct exec "$ctid" -- env \ - LC_ALL=C.UTF-8 \ - LANG=C.UTF-8 \ - SCRIPT_URL="$SCRIPT_URL" \ - PG_VERSION="$PG_VERSION" \ - DOCUMENTDB_TAG="$DOCUMENTDB_TAG" \ - DOCUMENTDB_DISTRO="$DOCUMENTDB_DISTRO" \ - FERRETDB_HOSTNAME="$HOSTNAME_LXC" \ - FERRETDB_LISTEN_ADDR="$FERRETDB_LISTEN_ADDR" \ - FERRETDB_USER="$FERRETDB_USER" \ - FERRETDB_PASSWORD="$FERRETDB_PASSWORD" \ - TS_AUTHKEY="$TS_AUTHKEY" \ - bash -s -- "$mode" -} - -# ============================================================ -# MODE: Proxmox host — create LXC + install -# ============================================================ -create_lxc() { - log_info "=== FerretDB — LXC creation ===" - - # Generate the password on the host so we can both pass it in and print it. - if [[ -z "$FERRETDB_PASSWORD" ]]; then - FERRETDB_PASSWORD=$(openssl rand -hex 24) - log_info "Generated FerretDB password (saved in the summary below)." - fi - - if [[ -z "$TEMPLATE" ]]; then - TEMPLATE=$(detect_latest_debian_template) - else - log_info "Using user-provided template: $TEMPLATE" - fi - ensure_template_present "$TEMPLATE" - - if [[ -z "$CTID" ]]; then - CTID=$(allocate_ctid) - log_info "Auto-selected CTID: $CTID" - fi - - log_info "Creating LXC ${CTID} (${HOSTNAME_LXC})..." - pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \ - --hostname "$HOSTNAME_LXC" \ - --cores "$CORES" \ - --memory "$RAM" \ - --rootfs "${STORAGE}:${DISK}" \ - --net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \ - --unprivileged 1 \ - --features "nesting=1" \ - --tags "infra-script,${LXC_TAG}" \ - --onboot 1 \ - --start 0 - - # Tailscale needs /dev/net/tun inside the unprivileged container. - log_info "Adding /dev/net/tun passthrough for Tailscale..." - cat >> "/etc/pve/lxc/${CTID}.conf" </dev/null; do - tries=$((tries + 1)) - if (( tries > 30 )); then - log_error "LXC ${CTID} did not acquire an IP after 30s." - exit 1 - fi - sleep 1 - done - - log_info "Running installer inside LXC ${CTID}..." - exec_in_lxc "$CTID" "--install" - - local ip - ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true) - - echo "" - log_info "=========================================" - log_info "LXC ${CTID} created successfully!" - log_info "=========================================" - echo "" - echo " Hostname : ${HOSTNAME_LXC}" - echo " IP : ${ip:-pending}" - echo " FerretDB : ${FERRETDB_LISTEN_ADDR}" - echo "" - echo "MongoDB connection string (point your client/app at it):" - echo " mongodb://${FERRETDB_USER}:${FERRETDB_PASSWORD}@${ip:-}:27017/" - echo "" - echo "Store this password somewhere safe — it is not persisted on the host:" - echo " user : ${FERRETDB_USER}" - echo " password : ${FERRETDB_PASSWORD}" - echo "" -} - -# ============================================================ -# MODE: Proxmox host — update existing LXC -# ============================================================ -update_lxc() { - local ctid="$1" - log_info "=== FerretDB — updating existing LXC ${ctid} ===" - - if ! pct status "$ctid" | grep -q running; then - log_info "Starting LXC ${ctid}..." - pct start "$ctid" - sleep 3 - fi - - log_info "Refreshing Debian packages inside LXC ${ctid}..." - pct exec "$ctid" -- sh -c "export DEBIAN_FRONTEND=noninteractive LC_ALL=C.UTF-8 LANG=C.UTF-8; apt-get update >/dev/null && apt-get upgrade -y >/dev/null" - - log_info "Upgrading FerretDB stack inside LXC ${ctid}..." - exec_in_lxc "$ctid" "--update" - - log_info "Update of LXC ${ctid} complete." -} - -# ============================================================ -# MODE: inside LXC — fresh install of FerretDB -# ============================================================ -install_inside_lxc() { - log_info "=== FerretDB — installation ===" - - if [[ -z "$FERRETDB_PASSWORD" ]]; then - FERRETDB_PASSWORD=$(openssl rand -hex 24 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n') - log_info "Generated FerretDB password (shown in the summary below)." - fi - - log_info "Updating package index..." - export DEBIAN_FRONTEND=noninteractive - apt-get update >/dev/null - apt-get upgrade -y >/dev/null - - log_info "Installing base dependencies..." - apt-get install -y curl jq ca-certificates gnupg lsb-release sudo logrotate openssl >/dev/null - - log_info "Installing Tailscale..." - if ! command -v tailscale >/dev/null 2>&1; then - curl -fsSL https://tailscale.com/install.sh | sh >/dev/null 2>&1 \ - || log_warn "Tailscale install script failed — install it manually later." - fi - systemctl enable --now tailscaled >/dev/null 2>&1 \ - || log_warn "tailscaled failed to start (is /dev/net/tun mapped into the LXC?)" - - install_or_upgrade_packages - - log_info "Configuring PostgreSQL for DocumentDB..." - local pg_confd="/etc/postgresql/${PG_VERSION}/main/conf.d" - mkdir -p "$pg_confd" - # https://docs.ferretdb.io/installation/documentdb/deb/ - cat > "${pg_confd}/documentdb.conf" </dev/null <<'SQL' -CREATE EXTENSION IF NOT EXISTS documentdb CASCADE; -SQL - - # Provision the user THROUGH DocumentDB — never a plain CREATE ROLE ... PASSWORD. - # DocumentDB builds the SCRAM-SHA-256 verifier with its own salt length - # (documentdb.scramDefaultSaltLen = 28 bytes) via documentdb_api.create_user / - # update_user. A native PostgreSQL role instead stores a 16-byte salt, which - # MongoDB clients reject at SASL step2 with "invalid salt length of 16". - # create_user only accepts a read-only role, or the clusterAdmin + - # readWriteAnyDatabase pair we use here for full read/write (FerretDB/DocumentDB - # commands/users.c::ValidateAndObtainUserRole). The spec is built with jq so the - # password is JSON-escaped, then embedded in a $DDB$-dollar-quoted SQL literal. - log_info "Provisioning MongoDB user '${FERRETDB_USER}' via DocumentDB (28-byte SCRAM salt)..." - local role_exists cmd spec - role_exists=$(su -s /bin/sh postgres -c \ - "psql -tAX -d postgres -c \"SELECT 1 FROM pg_roles WHERE rolname = '${FERRETDB_USER}'\"" 2>/dev/null || true) - if [[ "$role_exists" == "1" ]]; then - log_info "Role '${FERRETDB_USER}' already exists — resetting its password via DocumentDB." - cmd="update_user" - spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \ - '{updateUser:$u, pwd:$p}') - else - cmd="create_user" - spec=$(jq -nc --arg u "$FERRETDB_USER" --arg p "$FERRETDB_PASSWORD" \ - '{createUser:$u, pwd:$p, roles:[{role:"clusterAdmin",db:"admin"},{role:"readWriteAnyDatabase",db:"admin"}]}') - fi - su -s /bin/sh postgres -c "psql -v ON_ERROR_STOP=1 -d postgres" >/dev/null < /etc/systemd/system/ferretdb.service.d/override.conf </dev/null 2>&1 || true - systemctl enable ferretdb >/dev/null 2>&1 || true - # Explicit restart: the deb postinst may have already started ferretdb with - # default settings, in which case 'enable --now' would not re-read our override. - systemctl restart ferretdb || log_warn "ferretdb failed to start — check 'journalctl -u ferretdb'." - - # --- Log hygiene --- - # PostgreSQL ships /etc/logrotate.d/postgresql-common already. FerretDB logs to - # journald, so bound the journal instead of adding a logrotate stanza. - log_info "Bounding the systemd journal size..." - mkdir -p /etc/systemd/journald.conf.d - cat > /etc/systemd/journald.conf.d/ferretdb.conf <<'EOF' -[Journal] -SystemMaxUse=200M -EOF - systemctl restart systemd-journald >/dev/null 2>&1 || true - - # --- Console auto-login on tty1 (Proxmox web console / pct console) --- - log_info "Enabling console auto-login on tty1..." - mkdir -p /etc/systemd/system/container-getty@1.service.d - cat > /etc/systemd/system/container-getty@1.service.d/autologin.conf <<'EOF' -[Service] -ExecStart= -ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 $TERM -EOF - systemctl daemon-reload - systemctl restart container-getty@1.service 2>/dev/null || true - - configure_tailscale - - log_info "Configuring MOTD..." - # /etc/profile.d/ runs for every interactive login shell — works for both the - # auto-login tty and Tailscale SSH. Quoted heredoc except the values we want - # frozen at install time, which we inject via a small companion env file. - cat > /etc/ferretdb-motd.env < /etc/profile.d/00-ferretdb.sh <<'MOTD' -[ -f /etc/ferretdb-motd.env ] && . /etc/ferretdb-motd.env - -TS_FQDN=$(tailscale status --json 2>/dev/null | awk -F'"' ' - /"Self"/ { in_self=1 } - in_self && /"DNSName"/ { gsub(/\.$/, "", $4); print $4; exit } -') -[ -z "$TS_FQDN" ] && TS_FQDN="$(hostname).ts.net" -LAN_IP=$(ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1) -FERRET_VERSION=$(/usr/bin/ferretdb --version 2>/dev/null | head -n1 || echo "unknown") - -systemctl is-active --quiet ferretdb && FERRET_STATE="active" || FERRET_STATE="DOWN" -systemctl is-active --quiet postgresql && PG_STATE="active" || PG_STATE="DOWN" - -echo "" -echo " _____ _ ____ ____ " -echo "| ___|__ _ __ _ __ ___| |_| _ \\| __ ) " -echo "| |_ / _ \\ '__| '__/ _ \\ __| | | | _ \\ " -echo "| _| __/ | | | | __/ |_| |_| | |_) |" -echo "|_| \\___|_| |_| \\___|\\__|____/|____/ " -echo "" -echo "FerretDB (MongoDB-compatible) — ${FERRET_VERSION}" -echo "─────────────────────────────────────────" -echo "Status:" -echo " • FerretDB : ${FERRET_STATE} (listening on ${FERRETDB_LISTEN_ADDR})" -echo " • PostgreSQL : ${PG_STATE} (127.0.0.1:5432)" -echo "" -echo "Connection string (MongoDB URI):" -echo " mongodb://${FERRETDB_USER}:@${LAN_IP:-}:27017/" -echo " (tailnet) mongodb://${FERRETDB_USER}:@${TS_FQDN}:27017/" -echo "" -echo "Useful commands:" -echo " systemctl status ferretdb postgresql" -echo " journalctl -u ferretdb -f" -echo " mongosh \"mongodb://${FERRETDB_USER}:@127.0.0.1:27017/\"" -echo "─────────────────────────────────────────" -echo "" -MOTD - chmod +x /etc/profile.d/00-ferretdb.sh - - log_info "Cleaning up..." - apt-get clean >/dev/null 2>&1 || true - - local ip - ip=$(ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 || true) - - echo "" - log_info "=========================================" - log_info "FerretDB installation complete!" - log_info "=========================================" - echo "" - echo "MongoDB connection string (point your client/app at it):" - echo " mongodb://${FERRETDB_USER}:${FERRETDB_PASSWORD}@${ip:-}:27017/" - echo "" - echo "Credentials (store safely — not persisted on the Proxmox host):" - echo " user : ${FERRETDB_USER}" - echo " password : ${FERRETDB_PASSWORD}" - echo "" -} - -# ============================================================ -# MODE: inside LXC — update only -# ============================================================ -update_inside_lxc() { - log_info "=== FerretDB — update ===" - export DEBIAN_FRONTEND=noninteractive - apt-get update >/dev/null - apt-get upgrade -y >/dev/null - install_or_upgrade_packages - configure_tailscale - log_info "Update complete." -} - -# ============================================================ -# Main — dispatch on explicit mode flag or auto-detect context -# ============================================================ -main() { - case "${1:-}" in - --install) - install_inside_lxc - return - ;; - --update) - update_inside_lxc - return - ;; - esac - - if command -v pct >/dev/null 2>&1; then - # Running on a Proxmox host - require_root - preflight_script_url - - local existing="" - if existing=$(find_existing_lxc); then - log_info "Found existing FerretDB LXC (CTID ${existing}, hostname/tag match) — switching to update mode." - update_lxc "$existing" - else - create_lxc - fi - else - # Inside a container (no Proxmox tooling) - require_root - if [[ -x /usr/bin/ferretdb ]]; then - update_inside_lxc - else - install_inside_lxc - fi - fi -} - -main "$@" diff --git a/gitea-runner/README.md b/gitea-runner/README.md index 4eaae73..28921b1 100644 --- a/gitea-runner/README.md +++ b/gitea-runner/README.md @@ -20,7 +20,7 @@ Single script, three automatic modes: bash -c "$(curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh)" ``` -The script automatically creates an Alpine 3.23 LXC with Docker and act_runner. +The script automatically creates an Alpine LXC (template auto-detected from `pveam available`) with Docker and act_runner. #### Customization @@ -34,6 +34,7 @@ CTID=120 HOSTNAME=runner-02 CORES=4 RAM=4096 bash -c "$(curl -fsSL https://gitea |----------|---------|-------------| | `CTID` | auto | Container ID | | `RUNNER_HOSTNAME` | `gitea-runner` | LXC Hostname | +| `TEMPLATE` | auto-detected | Alpine template; auto-detected from `pveam available` | | `CORES` | `2` | CPU cores | | `RAM` | `2048` | RAM in MiB | | `DISK` | `8` | Disk in GB | @@ -59,7 +60,7 @@ From inside the LXC: curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh | bash ``` -The script detects that act_runner is already installed and switches to update mode automatically. +The script detects that act_runner is already installed and switches to update mode automatically. Re-running from the Proxmox host does the same, plus refreshes the LXC's Alpine packages first (`apk update && apk upgrade`). ### Architecture diff --git a/gitea-runner/install.sh b/gitea-runner/install.sh index d1a4b31..3d23e0b 100644 --- a/gitea-runner/install.sh +++ b/gitea-runner/install.sh @@ -8,15 +8,19 @@ set -euo pipefail # --- Config (override via environment) --- CTID="${CTID:-}" -HOSTNAME="${RUNNER_HOSTNAME:-gitea-runner}" -TEMPLATE="${TEMPLATE:-alpine-3.23-default_20260116_amd64.tar.xz}" +HOSTNAME_LXC="${RUNNER_HOSTNAME:-gitea-runner}" +TEMPLATE="${TEMPLATE:-}" # auto-detected when empty STORAGE="${STORAGE:-local-lvm}" TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local}" CORES="${CORES:-2}" RAM="${RAM:-2048}" DISK="${DISK:-8}" BRIDGE="${BRIDGE:-vmbr0}" -SCRIPT_URL="https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh" +LXC_TAG="${LXC_TAG:-gitea-runner}" # stable identifier for the container +# SCRIPT_URL is what the host-side flow pipes into the LXC. Override it when +# testing from a non-main branch, e.g. +# SCRIPT_URL="https://gitea.arnodo.fr/.../branch/chore/standardize-lxc-scripts/gitea-runner/install.sh" +SCRIPT_URL="${SCRIPT_URL:-https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/main/gitea-runner/install.sh}" GITEA_HOSTNAME="${GITEA_HOSTNAME:-gitea.taila5ad8.ts.net}" GITEA_API="https://gitea.com/api/v1/repos/gitea/act_runner/releases" VERSION_FILE="/opt/gitea-runner_version.txt" @@ -27,9 +31,47 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +# Logs go to stderr so callers can safely use $(fn) without capturing log noise. +log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; } +log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } + +require_root() { + if [[ "$(id -u)" -ne 0 ]]; then + log_error "This script must be run as root (current uid: $(id -u))." + log_error "On Proxmox, launch it from the host shell or via the Web UI shell, both of which run as root." + exit 1 + fi +} + +# ============================================================ +# Load shared helpers (lib/common.sh: detect_latest_alpine_template, +# enable_tty1_autologin, find_existing_lxc, refresh_os_packages). +# +# Same reasoning as openbao/install.sh: a local checkout has the file +# on disk right next to us, but the documented curl one-liner (host or +# piped into `pct exec` inside the LXC) has no BASH_SOURCE path worth +# trusting, so fall back to fetching lib/common.sh over HTTP next to +# SCRIPT_URL. The LXC already needs outbound network to curl this very +# script and to download the act_runner binary, so this adds no new +# failure mode. +# ============================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || true)" +LIB_COMMON_URL="$(dirname "$(dirname "$SCRIPT_URL")")/lib/common.sh" +if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/../lib/common.sh" ]]; then + source "${SCRIPT_DIR}/../lib/common.sh" +else + source <(curl -fsSL "$LIB_COMMON_URL") +fi + +# `source <(curl ...)` swallows curl failures: an empty stream still makes +# `source` return 0, so a 404/network error would otherwise only surface +# later as a confusing "command not found" for detect_latest_alpine_template +# et al. Fail loudly here instead, with the URL that was tried. +if ! declare -F detect_latest_alpine_template >/dev/null; then + log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})." + exit 1 +fi # --- Helpers --- get_latest_release() { @@ -67,12 +109,34 @@ download_runner() { echo "$release" > "$VERSION_FILE" } +# Inject the script into the container and execute it in the requested mode. +# Forwards the runtime configuration the inner invocation needs to reproduce +# what the user requested on the host (mirrors openbao/install.sh's helper +# of the same name). +exec_in_lxc() { + local ctid="$1" + local mode="$2" # --install or --update + + pct exec "$ctid" -- sh -c "apk add --no-cache bash curl jq ca-certificates > /dev/null 2>&1" + curl -fsSL "$SCRIPT_URL" \ + | pct exec "$ctid" -- env \ + SCRIPT_URL="$SCRIPT_URL" \ + GITEA_HOSTNAME="$GITEA_HOSTNAME" \ + bash -s -- "$mode" +} + # ============================================================ # MODE 1: Proxmox host — create LXC container # ============================================================ create_lxc() { log_info "=== Gitea Act Runner — LXC Creation ===" + if [[ -z "$TEMPLATE" ]]; then + TEMPLATE=$(detect_latest_alpine_template) + else + log_info "Using user-provided template: $TEMPLATE" + fi + # Auto-select next CTID if not specified if [[ -z "$CTID" ]]; then CTID=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null \ @@ -80,22 +144,18 @@ create_lxc() { log_info "Auto-selected CTID: $CTID" fi - # Download template if needed - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE"; then - log_info "Downloading template $TEMPLATE..." - pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" - fi + ensure_template_present "$TEMPLATE" - log_info "Creating LXC $CTID ($HOSTNAME)..." + log_info "Creating LXC $CTID ($HOSTNAME_LXC)..." pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \ - --hostname "$HOSTNAME" \ + --hostname "$HOSTNAME_LXC" \ --cores "$CORES" \ --memory "$RAM" \ --rootfs "${STORAGE}:${DISK}" \ --net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \ --unprivileged 1 \ --features nesting=1,keyctl=1 \ - --tags "infra-script,cicd" \ + --tags "infra-script,${LXC_TAG}" \ --start 0 log_info "Configuring LXC for Docker and Tailscale..." @@ -111,8 +171,7 @@ EOF sleep 5 log_info "Injecting install script into container..." - pct exec "$CTID" -- sh -c "apk add --no-cache bash curl jq > /dev/null 2>&1" - curl -fsSL "$SCRIPT_URL" | pct exec "$CTID" -- bash -s -- --install + exec_in_lxc "$CTID" "--install" local ip ip=$(pct exec "$CTID" -- ip -4 addr show eth0 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1) @@ -122,7 +181,7 @@ EOF log_info "LXC $CTID created successfully!" log_info "=========================================" echo "" - echo " Hostname : $HOSTNAME" + echo " Hostname : $HOSTNAME_LXC" echo " IP : ${ip:-pending}" echo "" echo "Next steps:" @@ -133,6 +192,28 @@ EOF echo "" } +# ============================================================ +# MODE 1b: Proxmox host — update an existing LXC +# ============================================================ +update_lxc() { + local ctid="$1" + log_info "=== Gitea Act Runner — updating existing LXC ${ctid} ===" + + if ! pct status "$ctid" | grep -q running; then + log_info "Starting LXC ${ctid}..." + pct start "$ctid" + sleep 3 + fi + + log_info "Refreshing Alpine packages inside LXC ${ctid}..." + pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null" + + log_info "Upgrading act_runner binary inside LXC ${ctid}..." + exec_in_lxc "$ctid" "--update" + + log_info "Update of LXC ${ctid} complete." +} + # ============================================================ # MODE 2: Inside LXC — fresh install # ============================================================ @@ -232,24 +313,7 @@ LOGROTATE ln -sf /usr/sbin/logrotate /etc/periodic/daily/logrotate 2>/dev/null || true - log_info "Enabling console auto-login on tty1..." - # Alpine ships busybox getty by default; agetty (from util-linux) is what - # supports --autologin. - apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null - - # Replace any existing tty1 entry, then append our autologin line. Doing it - # in two steps (delete + append) is more robust than an in-place sed against - # a pattern that may drift across Alpine releases. - sed -i '/^tty1::/d' /etc/inittab - echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab - - # Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot. - kill -HUP 1 2>/dev/null || true - - # Kick any getty/agetty still attached to tty1 so init respawns it *now* with - # the new line — otherwise the first web-console session lands on the stale - # process and the operator has to type `exit` once before autologin kicks in. - pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true + enable_tty1_autologin log_info "Cleaning up..." rm -rf /var/cache/apk/* @@ -275,6 +339,8 @@ LOGROTATE update_runner() { log_info "=== Gitea Act Runner — Update ===" + refresh_os_packages + local release release=$(get_latest_release) @@ -305,18 +371,37 @@ update_runner() { # Main — detect context # ============================================================ main() { - if [[ "${1:-}" == "--install" ]]; then - # Explicitly called in install mode (from pct exec) - install_runner - elif command -v pct &> /dev/null; then + case "${1:-}" in + --install) + install_runner + return + ;; + --update) + update_runner + return + ;; + esac + + if command -v pct &> /dev/null; then # We're on the Proxmox host - create_lxc - elif [[ -f /usr/local/bin/act_runner ]]; then - # act_runner exists — update mode - update_runner + require_root + local existing="" + if existing=$(find_existing_lxc); then + log_info "Found existing gitea-runner LXC (CTID ${existing}, hostname/tag match) — switching to update mode." + update_lxc "$existing" + else + create_lxc + fi else - # Fresh LXC — install mode - install_runner + # Inside a container (no Proxmox tooling) + require_root + if [[ -f /usr/local/bin/act_runner ]]; then + # act_runner exists — update mode + update_runner + else + # Fresh LXC — install mode + install_runner + fi fi } diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..8caad69 --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,120 @@ +# lib/common.sh - Shared helpers for Proxmox LXC creator scripts. +# +# Sourced (not executed) by openbao/install.sh and gitea-runner/install.sh. +# Assumes the sourcing script already defines log_info/log_warn/log_error +# (both scripts do, identically) — this file does not redefine them. +# +# Contract for future LXC creator scripts: +# - detect_latest_alpine_template(): Alpine only. A future Debian-based +# script needs its own detect_latest_debian_template() (same pattern: +# pveam available + sort -V + hardcoded fallback) — do not overload +# this function with an OS parameter. +# - enable_tty1_autologin(): implements the Alpine/OpenRC autologin +# mechanism (inittab + agetty). A future Debian-based script needs a +# distinct function (systemd container-getty override) rather than a +# branch inside this one. +# - find_existing_lxc(): OS-agnostic, works by tag/hostname via `pct +# config`. Reusable as-is by any LXC creator script. +# - refresh_os_packages(): Alpine only (apk update && apk upgrade). A +# future Debian-based script needs its own apt-get variant. +# +# Does not set shell options (set -e/-u/-o pipefail): a sourced file must +# not impose those on the caller's shell. Both openbao/install.sh and +# gitea-runner/install.sh already set them before sourcing this file. + +# ============================================================ +# #12 - Detect newest Alpine LXC template available from the Proxmox repos. +# Echoes the template filename. Falls back to a hardcoded known-good +# template if `pveam` is unavailable or returns nothing. +# ============================================================ +detect_latest_alpine_template() { + local tmpl + tmpl=$(pveam available --section system 2>/dev/null \ + | awk '/^system[[:space:]]+alpine-/ {print $2}' \ + | sort -V \ + | tail -n1) + + if [[ -z "$tmpl" ]]; then + log_warn "Could not query pveam; falling back to a known-good Alpine template." + tmpl="alpine-3.22-default_20250617_amd64.tar.xz" + fi + log_info "Selected Alpine template: $tmpl" + echo "$tmpl" +} + +# ============================================================ +# #14 - Enable root auto-login on tty1 for an Alpine/OpenRC LXC. +# Idempotent: safe to call on every install/update. +# ============================================================ +enable_tty1_autologin() { + log_info "Enabling console auto-login on tty1..." + # Alpine ships busybox getty by default; agetty (from util-linux) is what + # supports --autologin. + apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null + + # Replace any existing tty1 entry, then append our autologin line. Doing it + # in two steps (delete + append) is more robust than an in-place sed against + # a pattern that may drift across Alpine releases. + sed -i '/^tty1::/d' /etc/inittab + echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab + + # Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot. + kill -HUP 1 2>/dev/null || true + + # Kick any getty/agetty still attached to tty1 so init respawns it *now* with + # the new line — otherwise the first web-console session lands on the stale + # process and the operator has to type `exit` once before autologin kicks in. + pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true +} + +# ============================================================ +# #12 - Ensure the given template is downloaded to TEMPLATE_STORAGE, doing a +# `pveam update` first so a stale local cache doesn't silently settle for an +# older version than the one detect_latest_alpine_template() just picked. +# Expects TEMPLATE_STORAGE to be set by the caller. +# ============================================================ +ensure_template_present() { + local tmpl="$1" + if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then + log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..." + pveam update >/dev/null + pveam download "$TEMPLATE_STORAGE" "$tmpl" + else + log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}." + fi +} + +# ============================================================ +# #15 - Find an existing LXC by tag or hostname (host-side, requires pct). +# Echoes the CTID on match, returns 1 if none found. +# +# Expects HOSTNAME_LXC and LXC_TAG to be set by the caller (as openbao's +# find_existing_lxc already does). +# ============================================================ +find_existing_lxc() { + local id host tags + while read -r id _; do + [[ -z "$id" || "$id" == "VMID" ]] && continue + host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true) + tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true) + if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then + echo "$id" + return 0 + fi + done < <(pct list | awk 'NR>1 {print $1}') + return 1 +} + +# ============================================================ +# #15 - Refresh OS packages (Alpine: apk update && apk upgrade). +# Callable only from inside the LXC: this is a plain bash function in the +# current process, so it cannot run across a `pct exec ... sh -c` boundary +# without shipping its definition into the container. Host-side callers +# (see openbao/install.sh's update_lxc()) invoke apk update/upgrade inline +# via `pct exec` instead — do not try to dedupe that call site onto this +# function. +# ============================================================ +refresh_os_packages() { + log_info "Refreshing Alpine packages..." + apk update >/dev/null && apk upgrade >/dev/null +} diff --git a/openbao/README.md b/openbao/README.md index 184c629..06e27b0 100644 --- a/openbao/README.md +++ b/openbao/README.md @@ -110,7 +110,9 @@ curl -fsSL https://gitea.arnodo.fr/Damien/infra-scripts/raw/branch/feat/lxc-Open The script auto-detects the presence of `/usr/local/bin/bao` and switches to update mode. The OpenRC service is stopped, the binary is swapped (the old one -is kept as `bao.bak.`), then the service is restarted. +is kept as `bao.bak.`), then the service is restarted. Re-running from the +Proxmox host does the same, plus refreshes the LXC's Alpine packages first +(`apk update && apk upgrade`). ### Architecture diff --git a/openbao/install.sh b/openbao/install.sh index c8be7f3..bd6c495 100755 --- a/openbao/install.sh +++ b/openbao/install.sh @@ -56,6 +56,39 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1" >&2; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } +# ============================================================ +# Load shared helpers (lib/common.sh: detect_latest_alpine_template, +# enable_tty1_autologin, find_existing_lxc, refresh_os_packages). +# +# This script runs in three different contexts, only one of which has a +# real file on disk next to it: +# - local checkout (`bash openbao/install.sh`) -> lib/common.sh +# sits right there at ../lib/common.sh, source it straight from disk. +# - Proxmox host, documented one-liner (`bash -c "$(curl ... )"`) +# -> no checkout, no BASH_SOURCE path worth trusting. +# - inside the LXC (exec_in_lxc does `curl ... | pct exec ... bash -s --`) +# -> same story, script arrives on stdin. +# For the latter two we fetch lib/common.sh over HTTP, next to SCRIPT_URL. +# The LXC already needs outbound network to curl this very script and to +# download the bao binary, so this adds no new failure mode. +# ============================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-.}")" 2>/dev/null && pwd || true)" +LIB_COMMON_URL="$(dirname "$(dirname "$SCRIPT_URL")")/lib/common.sh" +if [[ -n "$SCRIPT_DIR" && -f "${SCRIPT_DIR}/../lib/common.sh" ]]; then + source "${SCRIPT_DIR}/../lib/common.sh" +else + source <(curl -fsSL "$LIB_COMMON_URL") +fi + +# `source <(curl ...)` swallows curl failures: an empty stream still makes +# `source` return 0, so a 404/network error would otherwise only surface +# later as a confusing "command not found" for detect_latest_alpine_template +# et al. Fail loudly here instead, with the URL that was tried. +if ! declare -F detect_latest_alpine_template >/dev/null; then + log_error "Failed to load lib/common.sh (tried: ${LIB_COMMON_URL})." + exit 1 +fi + # ============================================================ # Generic helpers # ============================================================ @@ -204,48 +237,6 @@ configure_tailscale_proxy() { # Proxmox-host helpers # ============================================================ -# Detect newest Alpine LXC template available from the Proxmox repos. -detect_latest_alpine_template() { - local tmpl - tmpl=$(pveam available --section system 2>/dev/null \ - | awk '/^system[[:space:]]+alpine-/ {print $2}' \ - | sort -V \ - | tail -n1) - - if [[ -z "$tmpl" ]]; then - log_warn "Could not query pveam; falling back to a known-good Alpine template." - tmpl="alpine-3.22-default_20250617_amd64.tar.xz" - fi - log_info "Selected Alpine template: $tmpl" - echo "$tmpl" -} - -# Find an existing LXC by tag or hostname. Echoes CTID, returns 1 if none. -find_existing_lxc() { - local id host tags - while read -r id _; do - [[ -z "$id" || "$id" == "VMID" ]] && continue - host=$(pct config "$id" 2>/dev/null | awk -F': ' '/^hostname:/ {print $2}' || true) - tags=$(pct config "$id" 2>/dev/null | awk -F': ' '/^tags:/ {print $2}' || true) - if [[ "$host" == "$HOSTNAME_LXC" ]] || [[ ",${tags//;/,}," == *",${LXC_TAG},"* ]]; then - echo "$id" - return 0 - fi - done < <(pct list | awk 'NR>1 {print $1}') - return 1 -} - -ensure_template_present() { - local tmpl="$1" - if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -q "$tmpl"; then - log_info "Downloading template ${tmpl} to storage ${TEMPLATE_STORAGE}..." - pveam update >/dev/null - pveam download "$TEMPLATE_STORAGE" "$tmpl" - else - log_info "Template ${tmpl} already present on ${TEMPLATE_STORAGE}." - fi -} - # Pick next available CTID if user did not provide one. allocate_ctid() { pvesh get /cluster/nextid 2>/dev/null \ @@ -363,6 +354,9 @@ update_lxc() { fi log_info "Refreshing Alpine packages inside LXC ${ctid}..." + # refresh_os_packages() is a bash function local to this process; it can't + # run over `pct exec ... sh -c` without shipping the function definition + # into the container, so this call site stays inline rather than dedupe. pct exec "$ctid" -- sh -c "apk update >/dev/null && apk upgrade >/dev/null" log_info "Upgrading bao binary inside LXC ${ctid}..." @@ -474,24 +468,7 @@ EOF log_info "Starting openbao service..." rc-service openbao start || log_warn "openbao failed to start — inspect /var/log/openbao.log" - log_info "Enabling console auto-login on tty1..." - # Alpine ships busybox getty by default; agetty (from util-linux) is what - # supports --autologin. - apk add --no-cache agetty >/dev/null 2>&1 || apk add --no-cache util-linux >/dev/null - - # Replace any existing tty1 entry, then append our autologin line. Doing it - # in two steps (delete + append) is more robust than an in-place sed against - # a pattern that may drift across Alpine releases. - sed -i '/^tty1::/d' /etc/inittab - echo 'tty1::respawn:/sbin/agetty --autologin root --noclear 38400 tty1' >> /etc/inittab - - # Tell PID 1 to re-read /etc/inittab so the change takes effect without a reboot. - kill -HUP 1 2>/dev/null || true - - # Kick any getty/agetty still attached to tty1 so init respawns it *now* with - # the new line — otherwise the first web-console session lands on the stale - # process and the operator has to type `exit` once before autologin kicks in. - pkill -KILL -f '(getty|agetty).*tty1' 2>/dev/null || true + enable_tty1_autologin configure_tailscale_proxy @@ -563,8 +540,7 @@ MOTD # ============================================================ update_inside_lxc() { log_info "=== OpenBao — update ===" - apk update >/dev/null - apk upgrade >/dev/null + refresh_os_packages install_or_upgrade_bao configure_tailscale_proxy log_info "Update complete."