#!/bin/bash set -euo pipefail # OSTree-Repo = Wurzel des Ausgabe-Volumes. nginx mountet genau dieses Volume # nach /usr/share/nginx/html/flatpak und liefert es unter /flatpak/ aus. REPO_PATH="/srv/flatpak" CACHE_DIR="/var/cache/flatpak-mirror" GIT_CACHE="$CACHE_DIR/sources-git" CONFIG_SRC="/config/flatpaks.yaml" ARCH="x86_64" : "${MIRROR_DOMAIN:?MIRROR_DOMAIN muss gesetzt sein}" : "${GPG_KEY_ID:?GPG_KEY_ID muss gesetzt sein (gemeinsamer Signing-Key, siehe .env)}" : "${MIRROR_REPO_ID:=company-mirror}" : "${SOURCES_GIT_REF:=main}" : "${SYNC_INTERVAL_SECONDS:=900}" export GNUPGHOME="${GNUPGHOME:-/etc/mirror-gpg}" mkdir -p "$CACHE_DIR" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } # --- 1. flatpaks.yaml besorgen --------------------------------------------- # Setzt die Git-Authentifizierung fuer das gemeinsame Freigabe-Repo. Bei # HTTPS+Token ueber GIT_CONFIG_*-Umgebungsvariablen (git >= 2.31): das Token # steht so weder in der URL (die in Logs auftauchen koennte) noch als # Kommandozeilenargument (das per "ps aux" auf dem Host sichtbar waere). configure_git_auth() { unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 if [ -n "${SOURCES_GIT_USERNAME:-}" ] && [ -n "${SOURCES_GIT_TOKEN:-}" ]; then log "Git: verwende Token-Authentifizierung (Benutzer: ${SOURCES_GIT_USERNAME})" export GIT_CONFIG_COUNT=1 export GIT_CONFIG_KEY_0="http.extraHeader" export GIT_CONFIG_VALUE_0="Authorization: Basic $(printf '%s' "${SOURCES_GIT_USERNAME}:${SOURCES_GIT_TOKEN}" | base64 | tr -d '\n')" fi if [ -n "${SOURCES_GIT_SSH_KEY:-}" ] && [ -f "${SOURCES_GIT_SSH_KEY}" ]; then log "Git: verwende SSH-Deploy-Key ${SOURCES_GIT_SSH_KEY}" export GIT_SSH_COMMAND="ssh -i ${SOURCES_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${CACHE_DIR}/known_hosts" fi } get_config() { if [ -n "${SOURCES_GIT_URL:-}" ]; then configure_git_auth if [ -d "$GIT_CACHE/.git" ]; then log "Git: hole Updates aus ${SOURCES_GIT_URL} (@ ${SOURCES_GIT_REF})" if ! git -C "$GIT_CACHE" fetch --quiet origin "$SOURCES_GIT_REF" \ || ! git -C "$GIT_CACHE" reset --hard --quiet "origin/${SOURCES_GIT_REF}"; then log "FEHLER: git fetch fehlgeschlagen." return 1 fi else log "Git: klone ${SOURCES_GIT_URL} (@ ${SOURCES_GIT_REF})" rm -rf "$GIT_CACHE" if ! git clone --quiet --branch "$SOURCES_GIT_REF" "$SOURCES_GIT_URL" "$GIT_CACHE"; then log "FEHLER: git clone fehlgeschlagen." return 1 fi fi CONFIG_FILE="$GIT_CACHE/flatpaks.yaml" else log "Kein SOURCES_GIT_URL gesetzt, nutze lokal gemountete ${CONFIG_SRC}" CONFIG_FILE="$CONFIG_SRC" fi if [ ! -f "$CONFIG_FILE" ]; then log "FEHLER: flatpaks.yaml nicht gefunden unter ${CONFIG_FILE}" return 1 fi } # --- 2. Repo initialisieren, falls noch nicht vorhanden -------------------- init_repo() { mkdir -p "$REPO_PATH" if [ ! -f "$REPO_PATH/config" ]; then log "Initialisiere neues OSTree-Repo unter ${REPO_PATH}" ostree --repo="$REPO_PATH" init --mode=archive-z2 fi if ! ostree --repo="$REPO_PATH" remote list | grep -q '^flathub$'; then log "Richte Flathub als Remote ein" if ! curl -sSL -o /tmp/flathub.gpg "$FLATHUB_GPG_URL"; then log "FEHLER: Flathub-GPG-Key konnte nicht von ${FLATHUB_GPG_URL} geladen werden." return 1 fi if ! ostree --repo="$REPO_PATH" remote add flathub "$FLATHUB_REMOTE_URL" \ --gpg-import=/tmp/flathub.gpg; then log "FEHLER: Flathub-Remote konnte nicht eingerichtet werden." return 1 fi fi } # --- 3. Explizit gewuenschte App-Refs aus flatpaks.yaml -------------------- explicit_app_refs() { yq -r '.apps[] | "app/" + .id + "/'"$ARCH"'/" + .branch' "$CONFIG_FILE" } # --- Hilfsfunktion: Runtime-Abhaengigkeit eines bereits gepullten Refs ----- # --- aus der eingebetteten "metadata"-Datei auslesen (Zeile runtime=...) --- get_runtime_dependency() { local ref="$1" local meta dep meta="$(ostree --repo="$REPO_PATH" cat "$ref" /metadata 2>/dev/null || true)" [ -z "$meta" ] && return 0 dep="$(echo "$meta" | grep -E '^runtime=' | head -n1 | cut -d= -f2-)" [ -z "$dep" ] && return 0 echo "runtime/${dep}" } # --- Hilfsfunktion: Commit-Checksum eines Refs finden, egal ob unter ------- # --- refs/heads/ oder (Collection-ID-gebunden) refs/mirrors// ---------- resolve_pulled_checksum() { local ref="$1" if [ -f "$REPO_PATH/refs/heads/$ref" ]; then cat "$REPO_PATH/refs/heads/$ref" return 0 fi find "$REPO_PATH/refs/mirrors" -type f -path "*/$ref" 2>/dev/null | head -n1 | xargs -r cat } # --- 4. Aktuell lokal verwaltete Refs ermitteln (Apps + Runtimes) ---------- current_refs() { ostree --repo="$REPO_PATH" refs | grep -E "^(app|runtime)/" || true } # --- 5. Pull + rekursive Abhaengigkeitsaufloesung -------------------------- # ACHTUNG: run_once() wird in der Hauptschleife als if-Bedingung aufgerufen. # Damit ist "set -e" fuer den gesamten Aufrufbaum darunter ausgehebelt (Bash # unterdrueckt den Abbruch in jedem Kontext, in dem der Rueckgabewert # ausgewertet wird - und zwar rekursiv). Fehler muessen hier deshalb # ausdruecklich geprueft und weitergereicht werden, sonst laeuft der Sync # stillschweigend weiter. # Pullt einen einzelnen Ref und legt den passenden heads/-Ref an # (siehe Kommentar bei resolve_pulled_checksum weiter oben). pull_one_ref() { local ref="$1" log "Pull: ${ref}" if ! ostree --repo="$REPO_PATH" pull --mirror flathub "$ref"; then log "FEHLER: Pull von ${ref} fehlgeschlagen." return 1 fi local checksum checksum="$(resolve_pulled_checksum "$ref")" if [ -z "$checksum" ]; then log "FEHLER: Konnte Commit-Checksum fuer ${ref} nach dem Pull nicht ermitteln." return 1 fi if ! ostree --repo="$REPO_PATH" refs --create="$ref" "$checksum" --force; then log "FEHLER: Konnte heads/-Ref fuer ${ref} nicht anlegen." return 1 fi # ref hat die Form "app/ID/ARCH/BRANCH" oder "runtime/ID/ARCH/BRANCH". # Signiert wird mit dem gemeinsamen Key aus ./gpg - demselben, mit dem der # DNF-Spiegel seine RPMs signiert. local ref_id ref_arch ref_branch ref_id="$(echo "$ref" | cut -d/ -f2)" ref_arch="$(echo "$ref" | cut -d/ -f3)" ref_branch="$(echo "$ref" | cut -d/ -f4)" # "flatpak build-sign" sucht die ID per Default unter app/ - eine Runtime # muss ausdruecklich als solche gekennzeichnet werden, sonst scheitert es # mit "Refspec 'app///' not found" und die Runtime bleibt # unsigniert (Clients mit GPG-Pruefung koennen sie dann nicht installieren). local -a sign_args=( --arch="$ref_arch" --gpg-sign="$GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" ) [ "${ref%%/*}" = "runtime" ] && sign_args+=(--runtime) log "Signiere ${ref} mit dem Spiegel-Key" if ! flatpak build-sign "$REPO_PATH" "$ref_id" "$ref_branch" "${sign_args[@]}"; then log "FEHLER: Signieren von ${ref} fehlgeschlagen." return 1 fi } # Ergebnis von resolve_and_pull_all(): alle Refs, die der Spiegel fuehren soll # (freigegebene Apps + alle aufgeloesten Runtimes). # # Bewusst eine globale Variable statt einer Ausgabe auf stdout: in der # Funktion laufen ostree und flatpak, deren eigene Ausgabe sonst ueber die # Kommandosubstitution mit ins "Ergebnis" geraet - im Log tauchten dann # saemtliche Fortschrittszeilen als vermeintliche Refs auf. WANTED_REFS=() # Nimmt die explizit freigegebenen App-Refs, pullt sie, liest jeweils die # Runtime-Abhaengigkeit aus und haengt sie hinten an die Warteschlange an - # so werden auch mehrstufige Abhaengigkeiten (App -> Runtime -> Basis-Runtime) # vollstaendig aufgeloest. # # Ein einzelner fehlgeschlagener Ref bricht den Lauf nicht ab: die uebrigen # Apps werden trotzdem aktualisiert, der Ref behaelt seinen letzten guten # Stand (er bleibt in WANTED_REFS und wird deshalb auch nicht geloescht) und # der Rueckgabewert markiert den Lauf als fehlgeschlagen. resolve_and_pull_all() { local -A seen=() local queue=() local ref dep status=0 WANTED_REFS=() while IFS= read -r ref; do [ -z "$ref" ] && continue queue+=("$ref") done < <(explicit_app_refs) while [ "${#queue[@]}" -gt 0 ]; do ref="${queue[0]}" queue=("${queue[@]:1}") [ -n "${seen[$ref]:-}" ] && continue seen["$ref"]=1 if ! pull_one_ref "$ref"; then log "${ref} wird uebersprungen, letzter guter Stand bleibt erhalten." status=1 fi # Die Runtime-Abhaengigkeit auch dann aufloesen, wenn der Ref gerade # fehlgeschlagen ist: sonst fehlte sie in WANTED_REFS und der Abgleich # in sync_refs() wuerde eine laengst gespiegelte Runtime loeschen, die # von der App weiterhin gebraucht wird. dep="$(get_runtime_dependency "$ref")" if [ -n "$dep" ] && [ -z "${seen[$dep]:-}" ]; then log " -> benoetigt zusaetzlich: ${dep}" queue+=("$dep") fi done [ "${#seen[@]}" -gt 0 ] && WANTED_REFS=("${!seen[@]}") return "$status" } # --- 6. Abgleich: pullen was fehlt (inkl. Abhaengigkeiten), loeschen was --- # --- nicht mehr gewuenscht ist ---------------------------------------------- sync_refs() { local wanted current ref status=0 log "Loese App-Refs und Abhaengigkeiten auf..." resolve_and_pull_all || status=1 # Schutz gegen eine leere/kaputte flatpaks.yaml (oder ein fehlgeschlagenes # yq): ohne diese Pruefung waere die Liste der gewuenschten Refs leer und # der Abgleich unten wuerde JEDEN Ref aus dem Spiegel loeschen. if [ "${#WANTED_REFS[@]}" -eq 0 ]; then log "FEHLER: flatpaks.yaml ergibt keinen einzigen App-Ref - es wird nichts" log "geloescht, der bisherige Stand des Repos bleibt unveraendert." return 1 fi wanted="$(printf '%s\n' "${WANTED_REFS[@]}")" current="$(current_refs)" log "Verwaltete Refs (Apps + Runtimes):" printf ' + %s\n' "${WANTED_REFS[@]}" # Refs entfernen, die weder explizit freigegeben noch als Abhaengigkeit # noch benoetigt werden while IFS= read -r ref; do [ -z "$ref" ] && continue if ! grep -qxF "$ref" <<< "$wanted"; then log "Entferne nicht mehr benoetigten Ref: ${ref}" if ! ostree --repo="$REPO_PATH" refs --delete "$ref"; then log "FEHLER: Ref ${ref} konnte nicht geloescht werden." status=1 fi fi done <<< "$current" return "$status" } # --- 7. Summary aktualisieren ----------------------------------------------- update_summary() { log "Aktualisiere Repo-Summary und Static Deltas" if ! flatpak build-update-repo \ --generate-static-deltas \ --gpg-sign="$GPG_KEY_ID" \ --gpg-homedir="$GNUPGHOME" \ "$REPO_PATH"; then log "FEHLER: Signierte Summary konnte nicht erstellt werden" return 1 fi } # --- 8. Client-Dateien schreiben -------------------------------------------- # Public Key + fertige .flatpakrepo-Datei, damit Clients den Spiegel mit # einem Kommando und MIT Signaturpruefung einbinden koennen. Beides liegt im # Repo-Verzeichnis und ist damit unter https:///flatpak/... abrufbar # (Flathub macht es mit flathub.gpg genauso). publish_client_files() { local keyfile="$REPO_PATH/${MIRROR_REPO_ID}.gpg" if ! gpg --batch --yes --export --output "$keyfile" "$GPG_KEY_ID"; then log "FEHLER: Public Key konnte nicht exportiert werden." return 1 fi cat > "$REPO_PATH/${MIRROR_REPO_ID}.flatpakrepo" <