349 lines
13 KiB
Bash
Executable File
349 lines
13 KiB
Bash
Executable File
#!/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
|