Initial commit

This commit is contained in:
2026-09-07 11:55:21 +02:00
commit d83087cbbd
20 changed files with 1963 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
#!/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/<id>/ ----------
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/<ID>/<ARCH>/<BRANCH>' 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://<domain>/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" <<EOF
[Flatpak Repo]
Title=${MIRROR_REPO_ID}
Url=https://${MIRROR_DOMAIN}/flatpak/
Homepage=https://${MIRROR_DOMAIN}/
Comment=Kuratierter Flatpak-Spiegel
GPGKey=$(base64 -w0 < "$keyfile")
EOF
log "Client-Dateien geschrieben: ${MIRROR_REPO_ID}.gpg, ${MIRROR_REPO_ID}.flatpakrepo"
}
run_once() {
local status=0
get_config || return 1
init_repo || return 1
# Ein einzelner nicht spiegelbarer Ref soll die uebrigen Apps nicht
# aufhalten: Summary und Client-Dateien werden trotzdem geschrieben, der
# Lauf gilt am Ende aber als fehlgeschlagen und wird wiederholt.
sync_refs || status=1
update_summary || return 1
publish_client_files || return 1
if [ "$status" -eq 0 ]; then
log "Sync abgeschlossen."
else
log "Sync abgeschlossen, aber mit Fehlern - siehe oben."
fi
return "$status"
}
# --- Hauptschleife ----------------------------------------------------------
log "Starte Sync-Loop (Intervall: ${SYNC_INTERVAL_SECONDS}s)"
while true; do
if run_once; then
log "Naechster Lauf in ${SYNC_INTERVAL_SECONDS}s"
else
log "Sync-Lauf fehlgeschlagen, versuche es beim naechsten Intervall erneut"
fi
sleep "$SYNC_INTERVAL_SECONDS"
done