Initial commit
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gemeinsam fuer beide Spiegel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Domain, unter der BEIDE Repos erreichbar sind:
|
||||||
|
# https://<MIRROR_DOMAIN>/flatpak/ und https://<MIRROR_DOMAIN>/dnf/
|
||||||
|
MIRROR_DOMAIN=repo.example.com
|
||||||
|
|
||||||
|
# Port, auf dem der gemeinsame nginx auf dem Host lauscht. Hinter einem
|
||||||
|
# vorgeschalteten Reverse Proxy z.B. auf 8080 legen.
|
||||||
|
HTTP_PORT=80
|
||||||
|
|
||||||
|
# Name/ID des Spiegels. Ergibt die Client-Dateien
|
||||||
|
# /dnf/<MIRROR_REPO_ID>.repo (dnf)
|
||||||
|
# /flatpak/<MIRROR_REPO_ID>.flatpakrepo (flatpak)
|
||||||
|
MIRROR_REPO_ID=company-mirror
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Signing-Key (fuer BEIDE Spiegel derselbe)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Der Key wird EXTERN erzeugt und als GnuPG-Homedir in den Ordner ./gpg neben
|
||||||
|
# dieser Datei gelegt. Beide Container mounten genau diesen Ordner read-only
|
||||||
|
# nach /run/secrets/gpg und kopieren ihn beim Start in ihr eigenes GNUPGHOME.
|
||||||
|
# Siehe README.md, Abschnitt "Signing-Key bereitstellen".
|
||||||
|
#
|
||||||
|
# ID des Keys, mit dem signiert wird - am besten der volle Fingerprint:
|
||||||
|
# gpg --list-secret-keys --keyid-format=long --with-colons
|
||||||
|
# Der Key darf KEINE Passphrase haben, sonst kann unbeaufsichtigt nicht
|
||||||
|
# signiert werden (weder rpmsign noch flatpak build-sign).
|
||||||
|
GPG_KEY_ID=0000000000000000000000000000000000000000
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Git-Repo mit den Freigabelisten (fuer BEIDE Spiegel dasselbe Repo)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Erwartet im Wurzelverzeichnis des Repos:
|
||||||
|
# flatpaks.yaml -> Freigabeliste des Flatpak-Spiegels
|
||||||
|
# packages.yaml -> Freigabeliste des DNF-Spiegels
|
||||||
|
# Vorlagen dafuer liegen in ./sources-repo-template/. Beide Container holen
|
||||||
|
# das Repo vor JEDEM Sync-Lauf neu, Aenderungen brauchen also keinen
|
||||||
|
# Container-Neubau/-Neustart.
|
||||||
|
SOURCES_GIT_URL=https://gitea.example.com/it/mirror-sources.git
|
||||||
|
SOURCES_GIT_REF=main
|
||||||
|
|
||||||
|
# Variante A - HTTPS mit Token (privates Repo). Das Token wird nie in die
|
||||||
|
# URL eingebettet, sondern per GIT_CONFIG_*-Header uebergeben.
|
||||||
|
SOURCES_GIT_USERNAME=
|
||||||
|
SOURCES_GIT_TOKEN=
|
||||||
|
|
||||||
|
# Variante B - SSH mit Deploy-Key: Keydatei in ./git-ssh/ ablegen (neben
|
||||||
|
# dieser Datei) und hier den Pfad IM CONTAINER eintragen. Leer lassen, wenn
|
||||||
|
# Variante A oder ein oeffentliches Repo genutzt wird.
|
||||||
|
SOURCES_GIT_SSH_KEY=
|
||||||
|
#SOURCES_GIT_SSH_KEY=/run/secrets/git-ssh/id_ed25519
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Nur Flatpak-Spiegel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Wie oft flatpaks.yaml geprueft und neue Refs gepullt werden. 900 = 15 Min.
|
||||||
|
FLATPAK_SYNC_INTERVAL_SECONDS=900
|
||||||
|
FLATHUB_REMOTE_URL=https://dl.flathub.org/repo/
|
||||||
|
FLATHUB_GPG_URL=https://dl.flathub.org/repo/flathub.gpg
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Nur DNF-Spiegel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Wie oft neu synchronisiert wird. 21600 = alle 6 Stunden.
|
||||||
|
DNF_SYNC_INTERVAL_SECONDS=21600
|
||||||
|
|
||||||
|
# AlmaLinux Release/Arch, gegen die auf "bereits vorhanden" geprueft wird.
|
||||||
|
# Muss zur Alma-Version des Images passen (siehe dnf-mirror/Dockerfile).
|
||||||
|
ALMA_RELEASEVER=10
|
||||||
|
ALMA_BASEARCH=x86_64
|
||||||
|
|
||||||
|
# Repo-IDs, die als "von Alma selbst aufloesbar" gelten. Pakete mit diesen
|
||||||
|
# Namen werden NICHT gespiegelt (Ausnahmen ueber force_include in
|
||||||
|
# packages.yaml).
|
||||||
|
ALMA_BASELINE_REPOIDS=baseos,appstream,extras,crb,epel
|
||||||
|
|
||||||
|
# Optional: gegen eigene Repo-Definitionen statt der im Image vorhandenen
|
||||||
|
# System-Repos pruefen (siehe dnf-mirror/alma-baseline.repo.d/).
|
||||||
|
ALMA_BASELINE_REPOSDIR=
|
||||||
|
#ALMA_BASELINE_REPOSDIR=/etc/mirror/alma-baseline.repo.d
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Zugangsdaten und Schluesselmaterial gehoeren nie ins Git-Repo.
|
||||||
|
.env
|
||||||
|
gpg/*
|
||||||
|
!gpg/.gitkeep
|
||||||
|
git-ssh/*
|
||||||
|
!git-ssh/.gitkeep
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
# Spiegelserver: Flatpak + DNF unter einer Domain
|
||||||
|
|
||||||
|
Ein Compose-Stack, zwei kuratierte Spiegel, ein Webserver, ein Signing-Key:
|
||||||
|
|
||||||
|
| Pfad | Inhalt | Container |
|
||||||
|
|------|--------|-----------|
|
||||||
|
| `https://<domain>/flatpak/` | gefilterter Flathub-Spiegel (OSTree) | `flatpak-mirror` |
|
||||||
|
| `https://<domain>/dnf/` | RPM-Spiegel fuer alles ohne Flatpak | `dnf-mirror` |
|
||||||
|
|
||||||
|
Beide Sync-Container schreiben nur in ihr eigenes Volume und haben keinen
|
||||||
|
Port nach aussen. Der gemeinsame `nginx` bekommt beide Volumes read-only
|
||||||
|
gemountet und liefert sie unter je einem Pfad aus.
|
||||||
|
|
||||||
|
Was sich beide teilen:
|
||||||
|
|
||||||
|
- **Denselben GPG-Signing-Key** - er wird extern erzeugt und beiden
|
||||||
|
Containern ueber dasselbe Verzeichnis `./gpg` hereingereicht. Clients
|
||||||
|
muessen damit nur *einem* Key vertrauen, egal ob Flatpak oder RPM.
|
||||||
|
- **Dasselbe Git-Repo** mit den Freigabelisten: `flatpaks.yaml` fuer den
|
||||||
|
Flatpak-Spiegel, `packages.yaml` fuer den DNF-Spiegel.
|
||||||
|
- **Denselben nginx** und damit dieselbe Domain und dasselbe TLS-Zertifikat.
|
||||||
|
|
||||||
|
## Erste Einrichtung
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Signing-Key bereitstellen
|
||||||
|
|
||||||
|
Der Key wird **extern** erzeugt (nicht mehr vom Container) und als
|
||||||
|
GnuPG-Homedir in den Ordner `./gpg` gelegt. Zum Beispiel auf einer
|
||||||
|
Admin-Maschine:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export GNUPGHOME=$(mktemp -d)
|
||||||
|
gpg --batch --passphrase '' --quick-generate-key 'Company Mirror <it@example.com>' rsa4096 sign never
|
||||||
|
gpg --list-secret-keys --keyid-format=long # Fingerprint notieren
|
||||||
|
rsync -a "$GNUPGHOME/" /pfad/zum/stack/gpg/
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann in der `.env` `GPG_KEY_ID` auf den Fingerprint setzen.
|
||||||
|
|
||||||
|
- Der Key darf **keine Passphrase** haben, sonst kann unbeaufsichtigt weder
|
||||||
|
`rpmsign` noch `flatpak build-sign` signieren.
|
||||||
|
- `./gpg` wird beiden Containern **read-only** nach `/run/secrets/gpg`
|
||||||
|
gemountet. Jeder Container kopiert das Homedir beim Start in sein eigenes
|
||||||
|
`GNUPGHOME` (`/etc/mirror-gpg`) - gpg schreibt naemlich in seinem Homedir
|
||||||
|
(trustdb, random_seed, Lockfiles), und so bleibt das Original unveraendert
|
||||||
|
und die beiden Container kommen sich beim Signieren nicht ins Gehege.
|
||||||
|
- `./gpg` auf dem Host nicht world-readable lassen (`chmod 700`) und
|
||||||
|
**sichern/backupen**: geht der Key verloren, muessen alle Clients den neuen
|
||||||
|
Public Key neu importieren - fuer beide Repos.
|
||||||
|
|
||||||
|
### 2. Freigabe-Git-Repo anlegen
|
||||||
|
|
||||||
|
Inhalt von `sources-repo-template/` (`flatpaks.yaml` + `packages.yaml`) in ein
|
||||||
|
eigenes Git-Repo pushen, anpassen und in der `.env` `SOURCES_GIT_URL` /
|
||||||
|
`SOURCES_GIT_REF` setzen. Fuer privaten Zugriff entweder
|
||||||
|
|
||||||
|
- **Token:** `SOURCES_GIT_USERNAME` + `SOURCES_GIT_TOKEN` (Read-only-PAT), oder
|
||||||
|
- **SSH:** Deploy-Key nach `./git-ssh/id_ed25519` legen (`chmod 600`) und
|
||||||
|
`SOURCES_GIT_SSH_KEY=/run/secrets/git-ssh/id_ed25519` setzen.
|
||||||
|
|
||||||
|
Das Token wird nie in die Git-URL eingebettet (URLs landen in Logs), sondern
|
||||||
|
per `GIT_CONFIG_*`-Header uebergeben - es ist damit auch nicht per `ps aux`
|
||||||
|
auf dem Host sichtbar. Im Container-Environment und in der `.env` steht es
|
||||||
|
trotzdem im Klartext, Dateirechte entsprechend setzen.
|
||||||
|
|
||||||
|
### 3. Starten
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up -d --build
|
||||||
|
docker compose logs -f flatpak-mirror dnf-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
Der erste Lauf dauert je nach Freigabeliste laenger (Firefox/LibreOffice sind
|
||||||
|
gross). Danach steht bereit:
|
||||||
|
|
||||||
|
| URL | Zweck |
|
||||||
|
|-----|-------|
|
||||||
|
| `https://<domain>/flatpak/<MIRROR_REPO_ID>.flatpakrepo` | fertige Remote-Definition inkl. Key |
|
||||||
|
| `https://<domain>/flatpak/<MIRROR_REPO_ID>.gpg` | Public Key (binaer, fuer OSTree) |
|
||||||
|
| `https://<domain>/dnf/<MIRROR_REPO_ID>.repo` | die eine `.repo`-Datei fuer Clients |
|
||||||
|
| `https://<domain>/dnf/RPM-GPG-KEY-mirror` | Public Key (ASCII-armored, fuer RPM) |
|
||||||
|
|
||||||
|
## Clients anbinden
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Flatpak
|
||||||
|
flatpak remote-add --if-not-exists company-mirror \
|
||||||
|
https://<domain>/flatpak/company-mirror.flatpakrepo
|
||||||
|
|
||||||
|
# DNF
|
||||||
|
rpm --import https://<domain>/dnf/RPM-GPG-KEY-mirror
|
||||||
|
curl -o /etc/yum.repos.d/company-mirror.repo \
|
||||||
|
https://<domain>/dnf/company-mirror.repo
|
||||||
|
pkcon refresh -c 1 # PackageKit/Discover hat einen eigenen Cache
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide Male ist es derselbe Key - er muss nur einmal als vertrauenswuerdig
|
||||||
|
eingestuft werden.
|
||||||
|
|
||||||
|
## Freigaben aendern
|
||||||
|
|
||||||
|
`flatpaks.yaml` bzw. `packages.yaml` im Freigabe-Repo bearbeiten, committen,
|
||||||
|
pushen. Beide Container ziehen das Repo vor jedem Sync-Lauf neu
|
||||||
|
(`FLATPAK_SYNC_INTERVAL_SECONDS`, Default 15 Min.;
|
||||||
|
`DNF_SYNC_INTERVAL_SECONDS`, Default 6 h). Fuer einen sofortigen Lauf:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose restart flatpak-mirror # bzw. dnf-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
## Funktionsprinzip
|
||||||
|
|
||||||
|
### Flatpak-Spiegel
|
||||||
|
|
||||||
|
1. `flatpaks.yaml` aus Git holen.
|
||||||
|
2. Fuer jede gelistete App den Ref `app/<id>/x86_64/<branch>` per
|
||||||
|
`ostree pull --mirror` von Flathub holen.
|
||||||
|
3. Aus der `metadata` des gepullten Refs die benoetigte Runtime auslesen und
|
||||||
|
rekursiv mitspiegeln (App -> Runtime -> Basis-Runtime).
|
||||||
|
4. Jeden Ref mit dem Spiegel-Key neu signieren (`flatpak build-sign`, fuer
|
||||||
|
`runtime/`-Refs mit `--runtime` - ohne das sucht flatpak die ID unter
|
||||||
|
`app/` und die Runtime bliebe unsigniert).
|
||||||
|
5. Refs loeschen, die weder freigegeben noch als Abhaengigkeit noetig sind.
|
||||||
|
6. Summary + Static Deltas erzeugen und signieren; `.flatpakrepo` und Public
|
||||||
|
Key ins Repo schreiben.
|
||||||
|
|
||||||
|
### DNF-Spiegel
|
||||||
|
|
||||||
|
1. `packages.yaml` aus Git holen.
|
||||||
|
2. Pro Fremdrepo per `dnf download` genau die dort gelisteten Paketnamen holen
|
||||||
|
(keine automatische Abhaengigkeitsaufloesung).
|
||||||
|
3. **Filtern:** Pakete, deren Name AlmaLinux selbst liefert
|
||||||
|
(`ALMA_BASELINE_REPOIDS`), werden verworfen - die Clients loesen sie aus
|
||||||
|
ihren eigenen Alma-Repos auf. Ausnahmen ueber `force_include`.
|
||||||
|
4. **Umsignieren** mit dem Spiegel-Key (`rpmsign --resign`), in einen privaten
|
||||||
|
Cache pro Fremdrepo.
|
||||||
|
5. Alle Fremdrepos zu **einem** oeffentlichen Repo zusammenfuehren - Clients
|
||||||
|
binden nur dieses eine ein.
|
||||||
|
6. `createrepo_c` + AppStream-Katalog (`appstreamcli compose`, fuer
|
||||||
|
Discover/GNOME Software), `repomd.xml` separat signieren
|
||||||
|
(`repo_gpgcheck=1`), Client-`.repo` schreiben.
|
||||||
|
|
||||||
|
## Verzeichnisstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose.yml beide Spiegel + der EINE nginx
|
||||||
|
.env.example gemeinsame Konfiguration beider Spiegel
|
||||||
|
gpg/ extern erzeugter Signing-Key (GnuPG-Homedir),
|
||||||
|
read-only in BEIDE Container gemountet
|
||||||
|
git-ssh/ optional: SSH-Deploy-Key fuers Freigabe-Repo
|
||||||
|
nginx/
|
||||||
|
conf.d/repo.conf ein Server-Block: /flatpak/ und /dnf/
|
||||||
|
flatpak-mirror/
|
||||||
|
Dockerfile debian-slim mit ostree, flatpak, git, yq
|
||||||
|
entrypoint.sh uebernimmt den gemeinsamen Key nach $GNUPGHOME
|
||||||
|
gpg-import.sh Key-Uebernahme + Signier-Selbsttest (identische
|
||||||
|
Kopie in dnf-mirror/, beide pflegen)
|
||||||
|
sync.sh Pull + Signieren + Summary + Client-Dateien
|
||||||
|
dnf-mirror/
|
||||||
|
Dockerfile AlmaLinux 10 mit createrepo_c, rpm-sign, appstream
|
||||||
|
entrypoint.sh uebernimmt den Key, droppt per setpriv auf "mirror"
|
||||||
|
gpg-import.sh identische Kopie aus flatpak-mirror/
|
||||||
|
run-as-mirror.sh Key pruefen/exportieren, Sync-Schleife
|
||||||
|
sync.sh Download -> Filtern -> Signieren -> Merge -> Metadaten
|
||||||
|
alma-baseline.repo.d/ optional: eigene Baseline-Repos fuer den Abgleich
|
||||||
|
sources-repo-template/ Vorlage fuer das gemeinsame Freigabe-Git-Repo
|
||||||
|
flatpaks.yaml
|
||||||
|
packages.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fehlersuche: "kein geheimer Schluessel ... gefunden"
|
||||||
|
|
||||||
|
Beide Container brechen beim Start ab, wenn sie mit `GPG_KEY_ID` nicht
|
||||||
|
signieren koennen. Die Meldung listet dann die Fingerprints, die im
|
||||||
|
Schluesselbund tatsaechlich liegen - meistens passt einfach `GPG_KEY_ID` in
|
||||||
|
der `.env` nicht dazu (Platzhalter aus `.env.example` stehengeblieben,
|
||||||
|
Key-ID statt Fingerprint, Tippfehler).
|
||||||
|
|
||||||
|
Direkt auf dem Host nachsehen, was in `./gpg` liegt:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gpg --homedir ./gpg --batch --list-secret-keys --keyid-format=long
|
||||||
|
```
|
||||||
|
|
||||||
|
- Kommt hier **`sec`** mit einem Fingerprint: genau diesen (ohne Leerzeichen)
|
||||||
|
als `GPG_KEY_ID` in die `.env` eintragen, dann `docker compose up -d`.
|
||||||
|
- Kommt **nur `pub`** und kein `sec`: im Ordner liegt nur der oeffentliche
|
||||||
|
Teil. `private-keys-v1.d/` muss mindestens eine `.key`-Datei enthalten -
|
||||||
|
sonst den Key neu erzeugen bzw. den geheimen Teil nachliefern.
|
||||||
|
- Kommt gar nichts: der Ordner ist kein GnuPG-Homedir, sondern enthaelt z.B.
|
||||||
|
eine exportierte `.asc`-Datei. Dann einmalig importieren:
|
||||||
|
```sh
|
||||||
|
gpg --homedir ./gpg --import /pfad/zum/secret-key.asc
|
||||||
|
```
|
||||||
|
- Meldet der Container "Signieren schlaegt fehl": der Key hat eine
|
||||||
|
Passphrase. Unbeaufsichtigtes Signieren braucht einen Key ohne Passphrase -
|
||||||
|
`gpg --homedir ./gpg --edit-key <fpr> passwd` und leer lassen.
|
||||||
|
|
||||||
|
Der Ordner wird beiden Containern read-only hereingereicht; sie kopieren
|
||||||
|
daraus nur das Schluesselmaterial (`pubring.kbx`, `private-keys-v1.d`,
|
||||||
|
`trustdb.gpg`, ...). Agent-Sockets (`S.gpg-agent` & Co.) und Lock-Dateien,
|
||||||
|
die vom gpg-agent auf dem Host stammen, koennen also gefahrlos darin liegen
|
||||||
|
bleiben - sie werden nicht mitkopiert.
|
||||||
|
|
||||||
|
## Bekannte Einschraenkungen
|
||||||
|
|
||||||
|
- **TLS** terminiert dieser Stack nicht. Entweder Zertifikate in den
|
||||||
|
nginx-Container mounten und einen `listen 443 ssl`-Block ergaenzen, oder -
|
||||||
|
meist einfacher - `HTTP_PORT` auf z.B. 8080 legen und einen vorhandenen
|
||||||
|
Reverse Proxy davorhaengen. Da beide Repos jetzt hinter *einem* nginx
|
||||||
|
liegen, reicht dafuer ein Zertifikat und ein Upstream.
|
||||||
|
- **Passphrase-geschuetzte Keys** sind nicht unterstuetzt (siehe oben).
|
||||||
|
- **AppStream/Discover:** Anwendungen ohne `.desktop`/`.metainfo.xml` bleiben
|
||||||
|
per `dnf`/`pkcon` installierbar, erscheinen aber ohne Katalogeintrag in
|
||||||
|
Discover. Reine CLI-Pakete lassen sich ueber `console_apps:` in
|
||||||
|
`packages.yaml` trotzdem in den Katalog aufnehmen. Der DNF-Teil ist auf
|
||||||
|
AlmaLinux 10+ ausgelegt (`appstreamcli compose` aus `appstream-compose`).
|
||||||
|
- **Namensbasierter Abgleich (DNF):** Ein Paket wird uebersprungen, sobald
|
||||||
|
Alma ein gleichnamiges hat - unabhaengig von der Version. Braucht eine
|
||||||
|
Anwendung eine neuere Version, hilft `force_include`.
|
||||||
|
- **Ein gemeinsamer Namensraum (DNF):** Zwei Fremdrepos mit gleichnamigem
|
||||||
|
Paket ueberschreiben sich im gemeinsamen Repo; das zuletzt gesyncte gewinnt.
|
||||||
|
- **Git-Ausfall:** Schlaegt der Fetch fehl, laeuft der DNF-Sync mit der
|
||||||
|
letzten erfolgreich geholten Liste weiter (nur beim allerersten Lauf bricht
|
||||||
|
er ab). Der Flatpak-Sync bricht den Lauf ab und versucht es beim naechsten
|
||||||
|
Intervall erneut.
|
||||||
|
- **Migration aus den beiden Einzel-Stacks:** Die alten Volumes werden nicht
|
||||||
|
weiterverwendet (andere Namen, andere Pfade im Container) - beide Spiegel
|
||||||
|
bauen sich beim ersten Lauf neu auf. Da jetzt ausserdem mit einem *neuen,
|
||||||
|
gemeinsamen* Key signiert wird, muessen die Clients ohnehin einmalig neu
|
||||||
|
onboardet werden.
|
||||||
|
- Der zusammengefuehrte Stack wurde nicht gegen echte Upstreams end-to-end
|
||||||
|
getestet (keine Docker-/Internet-Umgebung in dieser Session) - die Skripte
|
||||||
|
sind syntaktisch geprueft, der erste `docker compose up` sollte beobachtet
|
||||||
|
werden.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
FROM almalinux:10
|
||||||
|
|
||||||
|
# dnf-plugins-core -> dnf download, repoquery, config-manager
|
||||||
|
# createrepo_c -> Repo-Metadaten bauen
|
||||||
|
# rpm-sign, gnupg2 -> Pakete umsignieren
|
||||||
|
# git, openssh-clients -> packages.yaml vor jedem Sync aus dem gemeinsamen
|
||||||
|
# Freigabe-Git-Repo ziehen
|
||||||
|
# python3-pyyaml, jq -> packages.yaml (YAML) nach JSON wandeln und auswerten
|
||||||
|
# cpio -> Pakete fuer die AppStream-Erzeugung teilweise entpacken
|
||||||
|
# rsync -> privaten Paketstand pflegen + alle Fremdrepos zu
|
||||||
|
# einem gemeinsamen Repo zusammenfuehren
|
||||||
|
# util-linux-core -> stellt setpriv (root -> mirror-User droppen) sicher
|
||||||
|
RUN dnf -y install epel-release \
|
||||||
|
&& dnf -y install \
|
||||||
|
dnf-plugins-core \
|
||||||
|
createrepo_c \
|
||||||
|
rpm-sign \
|
||||||
|
gnupg2 \
|
||||||
|
git \
|
||||||
|
openssh-clients \
|
||||||
|
python3-pyyaml \
|
||||||
|
util-linux-core \
|
||||||
|
findutils \
|
||||||
|
cpio \
|
||||||
|
rsync \
|
||||||
|
procps-ng \
|
||||||
|
jq \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
# AppStream-Metadaten fuer Discover/GNOME Software werden mit `appstreamcli`
|
||||||
|
# gebaut. Dessen "compose"-Kommando steckt NICHT im Basispaket `appstream`,
|
||||||
|
# sondern im separaten Subpaket `appstream-compose` (gleiches AlmaLinux-10-
|
||||||
|
# AppStream-Repo). Ohne dieses Subpaket bricht `appstreamcli compose` zur
|
||||||
|
# Laufzeit mit "AppStream Compose binary ... was not found" ab.
|
||||||
|
RUN dnf -y install appstream appstream-compose \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
RUN useradd -r -m -d /var/lib/mirror mirror
|
||||||
|
|
||||||
|
COPY gpg-import.sh /usr/local/lib/gpg-import.sh
|
||||||
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
COPY run-as-mirror.sh /usr/local/bin/run-as-mirror.sh
|
||||||
|
COPY sync.sh /usr/local/bin/sync.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/run-as-mirror.sh /usr/local/bin/sync.sh
|
||||||
|
|
||||||
|
RUN mkdir -p /srv/dnf /var/cache/mirror /etc/mirror-gpg \
|
||||||
|
&& chown -R mirror:mirror /srv/dnf /var/cache/mirror /etc/mirror-gpg
|
||||||
|
|
||||||
|
ENV HOME=/var/lib/mirror
|
||||||
|
# Gemeinsamer Signing-Key: entrypoint.sh kopiert ihn beim Start aus dem
|
||||||
|
# read-only gemounteten /run/secrets/gpg hierher - derselbe Key, mit dem der
|
||||||
|
# flatpak-mirror-Container signiert.
|
||||||
|
ENV GNUPGHOME=/etc/mirror-gpg
|
||||||
|
|
||||||
|
# Bleibt root: entrypoint.sh uebernimmt den gemounteten Key und die
|
||||||
|
# Verzeichnisrechte und wechselt danach per setpriv fest zu "mirror". Alles
|
||||||
|
# Weitere (Sync-Loop, git, dnf download, gpg) laeuft unprivilegiert.
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# OPTIONAL. Standardmaessig fragt sync.sh fuer die "was kann Alma sowieso
|
||||||
|
# schon aufloesen?"-Pruefung einfach die im Mirror-Image bereits vorhandenen
|
||||||
|
# System-Repos ab (das Image ist selbst AlmaLinux, hat also baseos/appstream/
|
||||||
|
# extras/crb + epel schon konfiguriert) - dafuer ist diese Datei NICHT noetig.
|
||||||
|
#
|
||||||
|
# Setze die Env-Variable ALMA_BASELINE_REPOSDIR=/etc/mirror/alma-baseline.repo.d,
|
||||||
|
# wenn du stattdessen explizit gegen andere/eigene Repo-Definitionen pruefen
|
||||||
|
# willst (z.B. anderer Releasever, eigener interner Alma-Spiegel). Es wird
|
||||||
|
# daraus nie etwas gespiegelt, nur `dnf repoquery` dagegen ausgefuehrt.
|
||||||
|
|
||||||
|
[baseos]
|
||||||
|
name=AlmaLinux $releasever - BaseOS
|
||||||
|
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/baseos
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-10
|
||||||
|
enabled=1
|
||||||
|
|
||||||
|
[appstream]
|
||||||
|
name=AlmaLinux $releasever - AppStream
|
||||||
|
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/appstream
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-10
|
||||||
|
enabled=1
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
name=AlmaLinux $releasever - Extras
|
||||||
|
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/extras
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-10
|
||||||
|
enabled=1
|
||||||
|
|
||||||
|
[crb]
|
||||||
|
name=AlmaLinux $releasever - CRB
|
||||||
|
mirrorlist=https://mirrors.almalinux.org/mirrorlist/$releasever/crb
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-10
|
||||||
|
enabled=1
|
||||||
|
|
||||||
|
[epel]
|
||||||
|
name=Extra Packages for Enterprise Linux $releasever - $basearch
|
||||||
|
metalink=https://mirrors.fedoraproject.org/metalink?repo=epel-$releasever&arch=$basearch&infra=$infra&content=$contentdir
|
||||||
|
gpgcheck=1
|
||||||
|
gpgkey=https://dl.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-10
|
||||||
|
enabled=1
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Startet als root, uebernimmt den gemeinsamen, EXTERN erzeugten Signing-Key
|
||||||
|
# aus dem read-only gemounteten /run/secrets/gpg (auf dem Host: ./gpg neben
|
||||||
|
# der docker-compose.yml) in ein containerlokales GNUPGHOME, das dem
|
||||||
|
# unprivilegierten "mirror"-User gehoert - und wechselt dann per setpriv
|
||||||
|
# dauerhaft zu diesem User (ersetzt den Prozess direkt, kein Wrapper-
|
||||||
|
# Kindprozess, damit Signale/PID 1 sauber bleiben).
|
||||||
|
#
|
||||||
|
# Bewusst eine Kopie statt direkt auf dem Mount zu arbeiten: gpg schreibt in
|
||||||
|
# seinem Homedir (trustdb, random_seed, Lockfiles), und derselbe Ordner ist
|
||||||
|
# gleichzeitig im flatpak-mirror-Container gemountet. Die Kopie haelt das
|
||||||
|
# Original unveraendert und vermeidet, dass sich beide Container beim
|
||||||
|
# Signieren gegenseitig ins Gehege kommen.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GPG_SOURCE=/run/secrets/gpg
|
||||||
|
export GNUPGHOME="${GNUPGHOME:-/etc/mirror-gpg}"
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. /usr/local/lib/gpg-import.sh
|
||||||
|
|
||||||
|
import_shared_gpg
|
||||||
|
|
||||||
|
mkdir -p /srv/dnf /var/cache/mirror
|
||||||
|
chown -R mirror:mirror "$GNUPGHOME" /srv/dnf /var/cache/mirror
|
||||||
|
|
||||||
|
exec setpriv --reuid=mirror --regid=mirror --init-groups /usr/local/bin/run-as-mirror.sh
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Gemeinsame Hilfsfunktionen beider Spiegel-Container: den EXTERN erzeugten
|
||||||
|
# Signing-Key aus dem read-only gemounteten $GPG_SOURCE uebernehmen und
|
||||||
|
# pruefen, ob damit unbeaufsichtigt signiert werden kann.
|
||||||
|
#
|
||||||
|
# ACHTUNG: identische Kopie in dnf-mirror/gpg-import.sh - Aenderungen bitte in
|
||||||
|
# beiden pflegen. (Getrennte Build-Kontexte je Image; ein gemeinsamer Kontext
|
||||||
|
# wuerde das Schluesselverzeichnis ./gpg in den Build-Kontext ziehen.)
|
||||||
|
|
||||||
|
# Die Dateien, die ein GnuPG-Homedir ausmachen. Bewusst eine feste Liste statt
|
||||||
|
# "cp -a .": im Quellverzeichnis liegen oft noch die Agent-Sockets
|
||||||
|
# (S.gpg-agent & Co.) und Lock-Dateien eines gpg-agent, der auf dem HOST
|
||||||
|
# lief/laeuft. Die haben im Container nichts verloren.
|
||||||
|
GPG_HOME_ENTRIES=(pubring.kbx pubring.gpg secring.gpg trustdb.gpg \
|
||||||
|
private-keys-v1.d openpgp-revocs.d public-keys.d)
|
||||||
|
|
||||||
|
import_shared_gpg() {
|
||||||
|
if [ ! -d "$GPG_SOURCE" ]; then
|
||||||
|
echo "[gpg] FEHLER: $GPG_SOURCE ist nicht gemountet - fehlt der Ordner ./gpg?" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$GNUPGHOME"
|
||||||
|
mkdir -p "$GNUPGHOME"
|
||||||
|
chmod 700 "$GNUPGHOME"
|
||||||
|
|
||||||
|
local entry copied=0
|
||||||
|
for entry in "${GPG_HOME_ENTRIES[@]}"; do
|
||||||
|
[ -e "$GPG_SOURCE/$entry" ] || continue
|
||||||
|
# Kein "|| true": schlaegt das Kopieren des Schluesselmaterials fehl,
|
||||||
|
# soll das laut und sofort auffallen statt spaeter als "Key nicht
|
||||||
|
# gefunden".
|
||||||
|
cp -a "$GPG_SOURCE/$entry" "$GNUPGHOME/"
|
||||||
|
copied=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$copied" -eq 0 ]; then
|
||||||
|
echo "[gpg] FEHLER: in $GPG_SOURCE liegt kein GnuPG-Schluesselbund." >&2
|
||||||
|
echo "[gpg] Erwartet wird der INHALT eines GnuPG-Homedirs (pubring.kbx," >&2
|
||||||
|
echo "[gpg] private-keys-v1.d, ...), nicht z.B. eine exportierte .asc-Datei." >&2
|
||||||
|
echo "[gpg] Tatsaechlicher Inhalt:" >&2
|
||||||
|
ls -la "$GPG_SOURCE" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R go-rwx "$GNUPGHOME"
|
||||||
|
|
||||||
|
# Signiert wird unbeaufsichtigt, also nie ein Pinentry-Fenster erwarten.
|
||||||
|
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
|
||||||
|
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Listet die im Homedir tatsaechlich vorhandenen geheimen Schluessel als
|
||||||
|
# "<fingerprint> <uid>" - genau das, was als GPG_KEY_ID in die .env gehoert.
|
||||||
|
list_available_secret_keys() {
|
||||||
|
gpg --batch --list-secret-keys --with-colons 2>/dev/null | awk -F: '
|
||||||
|
$1 == "sec" { want = 1; fpr = ""; next }
|
||||||
|
$1 == "fpr" && want { fpr = $10; want = 0; next }
|
||||||
|
$1 == "uid" && fpr != "" { print " " fpr " " $10; fpr = ""; next }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prueft nicht nur, OB der Key da ist, sondern ob damit auch wirklich
|
||||||
|
# unbeaufsichtigt signiert werden kann - ein passphrasegeschuetzter Key faellt
|
||||||
|
# sonst erst mitten im ersten Sync-Lauf auf.
|
||||||
|
require_usable_signing_key() {
|
||||||
|
if ! gpg --batch --list-secret-keys "$GPG_KEY_ID" >/dev/null 2>&1; then
|
||||||
|
echo "[gpg] FEHLER: kein geheimer Schluessel '$GPG_KEY_ID' in ./gpg gefunden." >&2
|
||||||
|
local available
|
||||||
|
available="$(list_available_secret_keys)"
|
||||||
|
if [ -n "$available" ]; then
|
||||||
|
echo "[gpg] Vorhanden sind:" >&2
|
||||||
|
echo "$available" >&2
|
||||||
|
echo "[gpg] Einen dieser Fingerprints als GPG_KEY_ID in die .env eintragen." >&2
|
||||||
|
else
|
||||||
|
echo "[gpg] Der Schluesselbund enthaelt ueberhaupt keinen GEHEIMEN Schluessel" >&2
|
||||||
|
echo "[gpg] (nur oeffentliche?). private-keys-v1.d/ pruefen - dort muss" >&2
|
||||||
|
echo "[gpg] mindestens eine .key-Datei liegen." >&2
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local err
|
||||||
|
err="$(printf 'signing-selftest' \
|
||||||
|
| gpg --batch --yes --local-user "$GPG_KEY_ID" --detach-sign --output /dev/null - 2>&1)" && {
|
||||||
|
echo "[gpg] Gemeinsamen Signing-Key '$GPG_KEY_ID' uebernommen, Testsignatur ok."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[gpg] FEHLER: Schluessel '$GPG_KEY_ID' ist da, aber Signieren schlaegt fehl:" >&2
|
||||||
|
echo "$err" >&2
|
||||||
|
echo "[gpg] Haeufigste Ursache: der Key hat eine Passphrase. Unbeaufsichtigtes" >&2
|
||||||
|
echo "[gpg] Signieren braucht einen Key OHNE Passphrase (siehe README)." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Laeuft als unprivilegierter User "mirror" (siehe entrypoint.sh, der als root
|
||||||
|
# den gemeinsamen Signing-Key nach $GNUPGHOME kopiert und dann per setpriv
|
||||||
|
# hierher wechselt).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${GPG_KEY_ID:?GPG_KEY_ID muss gesetzt sein (gemeinsamer Signing-Key, siehe .env)}"
|
||||||
|
: "${MIRROR_REPO_ID:=company-mirror}"
|
||||||
|
: "${SYNC_INTERVAL_SECONDS:=21600}"
|
||||||
|
|
||||||
|
# Der Key wird NICHT vom Container erzeugt: beide Spiegel benutzen denselben,
|
||||||
|
# extern erzeugten Key aus ./gpg (den entrypoint.sh als root hierher kopiert
|
||||||
|
# hat). Ist er unbrauchbar, ist der ganze Sync sinnlos - lieber sofort und mit
|
||||||
|
# einer Meldung abbrechen, die sagt, was stattdessen im Schluesselbund liegt.
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. /usr/local/lib/gpg-import.sh
|
||||||
|
require_usable_signing_key
|
||||||
|
|
||||||
|
mkdir -p /srv/dnf
|
||||||
|
gpg --batch --yes --armor --export "$GPG_KEY_ID" > /srv/dnf/RPM-GPG-KEY-mirror
|
||||||
|
echo "[entrypoint] Public Key nach /srv/dnf/RPM-GPG-KEY-mirror exportiert."
|
||||||
|
|
||||||
|
term_handler() {
|
||||||
|
echo "[entrypoint] Beende nach aktuellem Sync-Lauf ..."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap term_handler SIGTERM SIGINT
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
/usr/local/bin/sync.sh || echo "[entrypoint] sync.sh ist mit Fehler beendet, naechster Versuch in ${SYNC_INTERVAL_SECONDS}s"
|
||||||
|
sleep "$SYNC_INTERVAL_SECONDS" &
|
||||||
|
wait $!
|
||||||
|
done
|
||||||
Executable
+661
@@ -0,0 +1,661 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Ein Sync-Durchlauf:
|
||||||
|
# 0. packages.yaml frisch aus Git ziehen (Fremdrepos + Paketnamen je Repo)
|
||||||
|
# 1. je Fremdrepo genau die dort gelisteten Pakete per `dnf download` holen,
|
||||||
|
# filtern (Alma-Baseline) und in einen privaten, pro Fremdrepo getrennten
|
||||||
|
# Paket-Cache signieren
|
||||||
|
# 2. ALLE Fremdrepos zu EINEM gemeinsamen, oeffentlichen Ausgabe-Repo
|
||||||
|
# zusammenfuehren (ein repodata, ein GPG-Key, eine Client-.repo-Datei) -
|
||||||
|
# Clients binden nur dieses eine Repo ein, unabhaengig davon, aus wie
|
||||||
|
# vielen Fremdrepos die Pakete urspruenglich stammen
|
||||||
|
# 3. Repo-Metadaten (createrepo_c) + AppStream-Metadaten (fuer Discover) bauen
|
||||||
|
# 4. repomd.xml signieren, Client-.repo-Datei schreiben
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
CACHE=/var/cache/mirror
|
||||||
|
OUT=/srv/dnf
|
||||||
|
SOURCES_GIT_CHECKOUT="$CACHE/sources-git"
|
||||||
|
PACKAGES_JSON="$CACHE/packages.json"
|
||||||
|
SIGNED_ROOT="$CACHE/signed"
|
||||||
|
|
||||||
|
: "${GPG_KEY_ID:?GPG_KEY_ID muss gesetzt sein (gemeinsamer Signing-Key, siehe .env)}"
|
||||||
|
: "${MIRROR_DOMAIN:?}"
|
||||||
|
: "${SOURCES_GIT_URL:?SOURCES_GIT_URL muss auf das Git-Repo mit packages.yaml zeigen}"
|
||||||
|
: "${SOURCES_GIT_REF:=main}"
|
||||||
|
: "${MIRROR_REPO_ID:=company-mirror}"
|
||||||
|
: "${ALMA_RELEASEVER:=10}"
|
||||||
|
: "${ALMA_BASEARCH:=x86_64}"
|
||||||
|
: "${ALMA_BASELINE_REPOIDS:=baseos,appstream,extras,crb,epel}"
|
||||||
|
|
||||||
|
mkdir -p "$CACHE/stage" "$CACHE/tmp-repos" "$SIGNED_ROOT" "$OUT"
|
||||||
|
|
||||||
|
log() { echo "[sync $(date '+%F %T')] $*"; }
|
||||||
|
|
||||||
|
# Zieht packages.yaml vor JEDEM Sync-Lauf frisch aus einem Git-Repo (statt aus
|
||||||
|
# lokal gemounteten Dateien), damit Aenderungen an der Paketliste ohne
|
||||||
|
# Container-Neubau/-Neustart wirksam werden.
|
||||||
|
fetch_packages_yaml() {
|
||||||
|
# HTTPS mit Token: ueber GIT_CONFIG_*-Umgebungsvariablen (git >= 2.31)
|
||||||
|
# statt "https://user:token@host/..." - so steht das Token weder in der
|
||||||
|
# URL (die in Logs auftauchen koennte) noch als Kommandozeilenargument
|
||||||
|
# (das per "ps aux" auf dem Host sichtbar waere).
|
||||||
|
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0
|
||||||
|
if [ -n "${SOURCES_GIT_USERNAME:-}" ] && [ -n "${SOURCES_GIT_TOKEN:-}" ]; then
|
||||||
|
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
|
||||||
|
export GIT_SSH_COMMAND="ssh -i ${SOURCES_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${CACHE}/known_hosts"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$SOURCES_GIT_CHECKOUT/.git" ]; then
|
||||||
|
log "Aktualisiere packages.yaml aus Git ($SOURCES_GIT_URL @ $SOURCES_GIT_REF) ..."
|
||||||
|
if git -C "$SOURCES_GIT_CHECKOUT" fetch --depth 1 origin "$SOURCES_GIT_REF" >>"$CACHE/git-sources.log" 2>&1 \
|
||||||
|
&& git -C "$SOURCES_GIT_CHECKOUT" reset --hard FETCH_HEAD >>"$CACHE/git-sources.log" 2>&1 \
|
||||||
|
&& git -C "$SOURCES_GIT_CHECKOUT" clean -fdx >>"$CACHE/git-sources.log" 2>&1; then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
log "WARNUNG: git fetch fehlgeschlagen, siehe $CACHE/git-sources.log - verwende letzten bekannten Stand von packages.yaml weiter."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Klone packages.yaml aus Git ($SOURCES_GIT_URL @ $SOURCES_GIT_REF) ..."
|
||||||
|
rm -rf "$SOURCES_GIT_CHECKOUT"
|
||||||
|
if ! git clone --branch "$SOURCES_GIT_REF" --single-branch --depth 1 \
|
||||||
|
"$SOURCES_GIT_URL" "$SOURCES_GIT_CHECKOUT" >>"$CACHE/git-sources.log" 2>&1; then
|
||||||
|
log "FEHLER: Erstklon von packages.yaml aus Git fehlgeschlagen, siehe $CACHE/git-sources.log"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local yaml_file="$SOURCES_GIT_CHECKOUT/packages.yaml"
|
||||||
|
if [ ! -f "$yaml_file" ]; then
|
||||||
|
log "FEHLER: $yaml_file existiert nicht im Git-Repo."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! python3 -c '
|
||||||
|
import sys, json, yaml
|
||||||
|
with open(sys.argv[1]) as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
data.setdefault("repos", [])
|
||||||
|
data.setdefault("force_include", [])
|
||||||
|
data.setdefault("console_apps", [])
|
||||||
|
json.dump(data, sys.stdout)
|
||||||
|
' "$yaml_file" > "$PACKAGES_JSON.tmp" 2>"$CACHE/packages-yaml-parse.log"; then
|
||||||
|
log "FEHLER: packages.yaml ist kein gueltiges YAML, siehe $CACHE/packages-yaml-parse.log"
|
||||||
|
rm -f "$PACKAGES_JSON.tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
mv "$PACKAGES_JSON.tmp" "$PACKAGES_JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_alma_names() {
|
||||||
|
log "Ermittle Paketnamen, die AlmaLinux selbst bereitstellt ..."
|
||||||
|
local -a enable_args=()
|
||||||
|
local id
|
||||||
|
IFS=',' read -ra ids <<< "$ALMA_BASELINE_REPOIDS"
|
||||||
|
for id in "${ids[@]}"; do
|
||||||
|
enable_args+=(--enablerepo="$id")
|
||||||
|
done
|
||||||
|
|
||||||
|
local -a reposdir_args=()
|
||||||
|
if [ -n "${ALMA_BASELINE_REPOSDIR:-}" ] && ls "${ALMA_BASELINE_REPOSDIR}"/*.repo >/dev/null 2>&1; then
|
||||||
|
reposdir_args=(--setopt="reposdir=${ALMA_BASELINE_REPOSDIR}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! dnf repoquery -y --quiet -a --qf '%{name}' \
|
||||||
|
--disablerepo='*' "${enable_args[@]}" \
|
||||||
|
--releasever="$ALMA_RELEASEVER" --forcearch="$ALMA_BASEARCH" \
|
||||||
|
"${reposdir_args[@]}" 2>>"$CACHE/repoquery.log" | sort -u > "$CACHE/alma-names.txt.tmp"; then
|
||||||
|
log "WARNUNG: repoquery gegen AlmaLinux-Baseline fehlgeschlagen, siehe $CACHE/repoquery.log"
|
||||||
|
log "Verwende ggf. vorherigen Stand von alma-names.txt weiter."
|
||||||
|
rm -f "$CACHE/alma-names.txt.tmp"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
mv "$CACHE/alma-names.txt.tmp" "$CACHE/alma-names.txt"
|
||||||
|
log "$(wc -l < "$CACHE/alma-names.txt") Paketnamen als 'ueber Alma aufloesbar' erfasst."
|
||||||
|
}
|
||||||
|
|
||||||
|
is_force_included() {
|
||||||
|
local name="$1" f
|
||||||
|
for f in "${FORCE_INCLUDE_NAMES[@]}"; do
|
||||||
|
[ "$f" = "$name" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Steht das Paket in console_apps: der packages.yaml? Dann bekommt es einen
|
||||||
|
# Katalogeintrag, auch wenn es (wie fast alle reinen CLI-Pakete) weder eine
|
||||||
|
# .desktop- noch eine .metainfo.xml-Datei mitbringt.
|
||||||
|
is_console_app() {
|
||||||
|
local name="$1" f
|
||||||
|
for f in "${CONSOLE_APP_NAMES[@]}"; do
|
||||||
|
[ "$f" = "$name" ] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Liest einen einzelnen Schluessel aus dem [Desktop Entry]-Abschnitt einer
|
||||||
|
# .desktop-Datei (ignoriert lokalisierte Varianten wie Name[de]= und andere
|
||||||
|
# Abschnitte wie [Desktop Action ...]).
|
||||||
|
desktop_get() {
|
||||||
|
local file="$1" key="$2"
|
||||||
|
awk -v key="$key" '
|
||||||
|
/^\[Desktop Entry\]/ { insec=1; next }
|
||||||
|
/^\[/ { insec=0 }
|
||||||
|
insec && index($0, key "=") == 1 { sub("^" key "=", ""); print; exit }
|
||||||
|
' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ACHTUNG: Ab bash 5.2 (AlmaLinux 10 liefert 5.2.26) steht ein unmaskiertes
|
||||||
|
# "&" im ERSETZUNGS-Teil von ${var//muster/ersatz} - wie bei sed - fuer den
|
||||||
|
# gefundenen Text. "${s//</<}" wuerde also "<lt;" ergeben statt "<".
|
||||||
|
# Die Ampersands muessen hier deshalb mit \& maskiert werden.
|
||||||
|
xml_escape() {
|
||||||
|
local s="$1"
|
||||||
|
s="${s//&/\&}"
|
||||||
|
s="${s//</\<}"
|
||||||
|
s="${s//>/\>}"
|
||||||
|
s="${s//\"/\"}"
|
||||||
|
s="${s//\'/\'}"
|
||||||
|
printf '%s' "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Erzeugt fuer ein Paket, das in console_apps: der packages.yaml steht, eine
|
||||||
|
# console-application-Metainfo aus dem RPM-Header. Reine CLI-Pakete (dotnet-sdk,
|
||||||
|
# terraform, kubectl, ...) bringen praktisch nie eine eigene .metainfo.xml oder
|
||||||
|
# .desktop-Datei mit und tauchen ohne Katalogeintrag in Discover ueberhaupt
|
||||||
|
# nicht auf - auch nicht bei exakter Suche nach dem Paketnamen, denn Discovers
|
||||||
|
# PackageKit-Backend durchsucht ausschliesslich den AppStream-Pool.
|
||||||
|
#
|
||||||
|
# BEWUSST nur opt-in ueber packages.yaml: wuerde man das fuer jedes Paket ohne
|
||||||
|
# .desktop-Datei tun, landeten auch saemtliche Runtime-, Bibliotheks- und
|
||||||
|
# Debug-Unterpakete als "Anwendungen" im Katalog.
|
||||||
|
#
|
||||||
|
# Args: rpm-datei pkgname composeroot dateiliste-des-pakets
|
||||||
|
generate_console_metainfo() {
|
||||||
|
local rpm="$1" pkgname="$2" composeroot="$3" filelist="$4"
|
||||||
|
local metadir="$composeroot/usr/share/metainfo"
|
||||||
|
local target="$metadir/${pkgname}.metainfo.xml"
|
||||||
|
local summary description url license binary
|
||||||
|
|
||||||
|
mkdir -p "$metadir"
|
||||||
|
[ -e "$target" ] && return
|
||||||
|
|
||||||
|
summary=$(rpm -qp --qf '%{SUMMARY}' "$rpm" 2>/dev/null)
|
||||||
|
description=$(rpm -qp --qf '%{DESCRIPTION}' "$rpm" 2>/dev/null)
|
||||||
|
url=$(rpm -qp --qf '%{URL}' "$rpm" 2>/dev/null)
|
||||||
|
license=$(rpm -qp --qf '%{LICENSE}' "$rpm" 2>/dev/null)
|
||||||
|
[ -z "$summary" ] || [ "$summary" = "(none)" ] && summary="$pkgname"
|
||||||
|
[ -z "$description" ] || [ "$description" = "(none)" ] && description="$summary"
|
||||||
|
[ "$url" = "(none)" ] && url=""
|
||||||
|
[ -z "$license" ] || [ "$license" = "(none)" ] && license="LicenseRef-proprietary"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
echo '<component type="console-application">'
|
||||||
|
echo " <id>$(xml_escape "$pkgname")</id>"
|
||||||
|
echo " <pkgname>$(xml_escape "$pkgname")</pkgname>"
|
||||||
|
echo ' <metadata_license>CC0-1.0</metadata_license>'
|
||||||
|
echo " <project_license>$(xml_escape "$license")</project_license>"
|
||||||
|
echo " <name>$(xml_escape "$pkgname")</name>"
|
||||||
|
echo " <summary>$(xml_escape "$summary")</summary>"
|
||||||
|
echo ' <description>'
|
||||||
|
# %{DESCRIPTION} ist Fliesstext mit Leerzeilen als Absatztrennern -
|
||||||
|
# AppStream verlangt hier <p>-Elemente.
|
||||||
|
printf '%s\n' "$description" | awk '
|
||||||
|
NF == 0 { if (para != "") { print para; para = "" }; next }
|
||||||
|
{ para = (para == "" ? $0 : para " " $0) }
|
||||||
|
END { if (para != "") print para }
|
||||||
|
' | while IFS= read -r para; do
|
||||||
|
echo " <p>$(xml_escape "$para")</p>"
|
||||||
|
done
|
||||||
|
echo ' </description>'
|
||||||
|
[ -n "$url" ] && echo " <url type=\"homepage\">$(xml_escape "$url")</url>"
|
||||||
|
|
||||||
|
# <provides><binary> laesst Discover/appstreamcli die Komponente auch
|
||||||
|
# ueber den Befehlsnamen finden, nicht nur ueber den Paketnamen.
|
||||||
|
local -a binaries=()
|
||||||
|
while IFS= read -r binary; do
|
||||||
|
[ -n "$binary" ] && binaries+=("$binary")
|
||||||
|
done < <(grep -E '^\./usr/(local/)?s?bin/[^/]+$' "$filelist" 2>/dev/null | sed 's|.*/||' | sort -u)
|
||||||
|
if [ "${#binaries[@]}" -gt 0 ]; then
|
||||||
|
echo ' <provides>'
|
||||||
|
for binary in "${binaries[@]}"; do
|
||||||
|
echo " <binary>$(xml_escape "$binary")</binary>"
|
||||||
|
done
|
||||||
|
echo ' </provides>'
|
||||||
|
fi
|
||||||
|
echo '</component>'
|
||||||
|
} > "$target"
|
||||||
|
}
|
||||||
|
|
||||||
|
# appstreamcli compose (AppStream 1.0+) ignoriert .desktop-Dateien komplett,
|
||||||
|
# wenn keine begleitende .metainfo.xml existiert - anders als das aeltere
|
||||||
|
# appstream-builder, das genau das frueher automatisch synthetisiert hat.
|
||||||
|
# Die meisten unabhaengigen Drittanbieter-RPMs (z.B. jpackage-basierte Apps
|
||||||
|
# wie XPipe) liefern aber nur eine .desktop-Datei mit. Deshalb hier selbst
|
||||||
|
# eine minimale Ersatz-Metainfo aus den Desktop-Datei-Feldern erzeugen, damit
|
||||||
|
# die Anwendung ueberhaupt einen Katalogeintrag in Discover bekommt. Bringt
|
||||||
|
# ein Paket schon eine echte .metainfo.xml mit (die auf dasselbe Desktop-File
|
||||||
|
# verweist), hat diese immer Vorrang - hier wird dann nichts generiert.
|
||||||
|
generate_fallback_metainfo() {
|
||||||
|
local composeroot="$1" pkgmap="$2"
|
||||||
|
local appdir="$composeroot/usr/share/applications"
|
||||||
|
local metadir="$composeroot/usr/share/metainfo"
|
||||||
|
local desktop_file base name summary categories cat pkgname
|
||||||
|
|
||||||
|
[ -d "$appdir" ] || return
|
||||||
|
mkdir -p "$metadir"
|
||||||
|
|
||||||
|
for desktop_file in "$appdir"/*.desktop; do
|
||||||
|
[ -e "$desktop_file" ] || continue
|
||||||
|
base=$(basename "$desktop_file")
|
||||||
|
|
||||||
|
if grep -rlq "type=\"desktop-id\">${base}<" "$metadir" 2>/dev/null; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
pkgname=""
|
||||||
|
[ -f "$pkgmap" ] && pkgname=$(awk -F'\t' -v b="$base" '$1 == b {print $2; exit}' "$pkgmap")
|
||||||
|
[ -z "$pkgname" ] && pkgname="${base%.desktop}"
|
||||||
|
|
||||||
|
name=$(desktop_get "$desktop_file" "Name")
|
||||||
|
summary=$(desktop_get "$desktop_file" "Comment")
|
||||||
|
categories=$(desktop_get "$desktop_file" "Categories")
|
||||||
|
[ -z "$name" ] && name="${base%.desktop}"
|
||||||
|
[ -z "$summary" ] && summary="$name"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo '<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
echo '<component type="desktop-application">'
|
||||||
|
echo " <id>${base}</id>"
|
||||||
|
echo " <pkgname>$(xml_escape "$pkgname")</pkgname>"
|
||||||
|
echo ' <metadata_license>CC0-1.0</metadata_license>'
|
||||||
|
echo ' <project_license>LicenseRef-proprietary</project_license>'
|
||||||
|
echo " <name>$(xml_escape "$name")</name>"
|
||||||
|
echo " <summary>$(xml_escape "$summary")</summary>"
|
||||||
|
echo " <description>"
|
||||||
|
echo " <p>$(xml_escape "$summary")</p>"
|
||||||
|
echo " </description>"
|
||||||
|
echo " <launchable type=\"desktop-id\">${base}</launchable>"
|
||||||
|
if [ -n "$categories" ]; then
|
||||||
|
echo ' <categories>'
|
||||||
|
local IFS=';'
|
||||||
|
for cat in $categories; do
|
||||||
|
[ -z "$cat" ] && continue
|
||||||
|
echo " <category>$(xml_escape "$cat")</category>"
|
||||||
|
done
|
||||||
|
echo ' </categories>'
|
||||||
|
fi
|
||||||
|
echo '</component>'
|
||||||
|
} > "$metadir/${base%.desktop}.metainfo.xml"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Baut den AppStream-Katalog mit `appstreamcli compose` (Paket
|
||||||
|
# appstream-compose). Das Kommando arbeitet nicht auf .rpm-Dateien, sondern auf
|
||||||
|
# einem Verzeichnisbaum wie einem Dateisystem-Root (/usr/share/applications,
|
||||||
|
# /usr/share/metainfo, ...). Wir entpacken deshalb erst die dafuer relevanten
|
||||||
|
# Pfade aus allen Paketen in so einen Baum und rufen dann `appstreamcli compose`
|
||||||
|
# darauf auf.
|
||||||
|
generate_appstream_via_compose() {
|
||||||
|
local id="$1" out="$2"
|
||||||
|
local composeroot="$CACHE/appstream-root/$id"
|
||||||
|
local asout="$CACHE/appstream/$id"
|
||||||
|
local pkgmap="$CACHE/appstream-pkgmap-$id.txt"
|
||||||
|
local rpm
|
||||||
|
|
||||||
|
rm -rf "$composeroot"
|
||||||
|
mkdir -p "$composeroot/usr/share/applications" "$composeroot/usr/share/metainfo"
|
||||||
|
: > "$pkgmap"
|
||||||
|
|
||||||
|
for rpm in "$out"/*.rpm; do
|
||||||
|
[ -e "$rpm" ] || continue
|
||||||
|
local pkgname
|
||||||
|
pkgname=$(rpm -qp --qf '%{NAME}' "$rpm" 2>/dev/null)
|
||||||
|
|
||||||
|
# Icons/Pixmaps liegen so gut wie immer am Standard-FHS-Pfad - dafuer
|
||||||
|
# reicht ein blinder Glob-Extract, eine Paketzuordnung brauchen wir
|
||||||
|
# dafuer nicht.
|
||||||
|
(cd "$composeroot" && rpm2cpio "$rpm" | cpio -idm --quiet \
|
||||||
|
'./usr/share/icons/*' \
|
||||||
|
'./usr/share/pixmaps/*') >>"$CACHE/appstream-$id.log" 2>&1
|
||||||
|
|
||||||
|
# .desktop-/AppStream-Dateien koennen dagegen auch ausserhalb der
|
||||||
|
# Standardpfade liegen (u.a. alle jpackage-basierten Java/JavaFX-Apps
|
||||||
|
# wie z.B. XPipe legen sie direkt im eigenen Installationsverzeichnis
|
||||||
|
# ab, z.B. /opt/<app>/<app>.desktop, und verschieben sie erst per
|
||||||
|
# %post-Skript - das sehen wir beim reinen Lesen des Pakets nicht).
|
||||||
|
# Deshalb IMMER das ganze Paket nur AUFLISTEN (kein teurer Voll-
|
||||||
|
# Extract, bei z.T. >100 MB grossen JRE-Bundles) und alle
|
||||||
|
# .desktop/.metainfo.xml/.appdata.xml-Dateien gezielt herausgreifen -
|
||||||
|
# dabei merken wir uns, aus welchem RPM-Paket jede .desktop-Datei
|
||||||
|
# stammt, fuer <pkgname> in der (Fallback-)Metainfo weiter unten.
|
||||||
|
local filelist matches
|
||||||
|
filelist=$(mktemp)
|
||||||
|
rpm2cpio "$rpm" 2>/dev/null | cpio -t --quiet > "$filelist" 2>/dev/null
|
||||||
|
mapfile -t matches < <(grep -E '\.desktop$|\.metainfo\.xml$|\.appdata\.xml$' "$filelist")
|
||||||
|
|
||||||
|
# Reines CLI-Paket, das in console_apps: der packages.yaml steht: aus
|
||||||
|
# dem RPM-Header eine console-application-Metainfo bauen. Bringt das
|
||||||
|
# Paket wider Erwarten doch eigene AppStream-/Desktop-Dateien mit,
|
||||||
|
# haben die Vorrang und wir erzeugen nichts.
|
||||||
|
if [ "${#matches[@]}" -eq 0 ] && is_console_app "$pkgname"; then
|
||||||
|
log "$pkgname: erzeuge Konsolen-Katalogeintrag (kein .desktop/.metainfo.xml im Paket)."
|
||||||
|
generate_console_metainfo "$rpm" "$pkgname" "$composeroot" "$filelist"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${#matches[@]}" -gt 0 ]; then
|
||||||
|
local extractroot f base destdir
|
||||||
|
extractroot=$(mktemp -d)
|
||||||
|
(cd "$extractroot" && rpm2cpio "$rpm" | cpio -idm --quiet "${matches[@]}") >>"$CACHE/appstream-$id.log" 2>&1
|
||||||
|
for f in "${matches[@]}"; do
|
||||||
|
[ -f "$extractroot/$f" ] || continue
|
||||||
|
base=$(basename "$f")
|
||||||
|
case "$base" in
|
||||||
|
*.desktop)
|
||||||
|
destdir="$composeroot/usr/share/applications"
|
||||||
|
printf '%s\t%s\n' "$base" "$pkgname" >> "$pkgmap"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
destdir="$composeroot/usr/share/metainfo"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# Nicht ueberschreiben, falls bereits ein anderes Paket schon
|
||||||
|
# eine gleichnamige Datei mitgebracht hat.
|
||||||
|
if [ ! -e "$destdir/$base" ]; then
|
||||||
|
cp "$extractroot/$f" "$destdir/$base"
|
||||||
|
# Vom Hersteller mitgelieferte .metainfo.xml/.appdata.xml
|
||||||
|
# haben so gut wie nie ein <pkgname> (das kennt nur die
|
||||||
|
# Distro/das Repo, nicht der Upstream-Entwickler) - ohne
|
||||||
|
# das bleibt die Komponente in Discover trotz gueltiger
|
||||||
|
# Metainfo nicht installierbar. Deshalb hier direkt
|
||||||
|
# ergaenzen, falls es fehlt.
|
||||||
|
if [ "$base" != "${base%.desktop}" ]; then
|
||||||
|
: # ist eine .desktop-Datei, braucht selbst kein <pkgname>
|
||||||
|
elif ! grep -q '<pkgname>' "$destdir/$base" 2>/dev/null; then
|
||||||
|
sed -i "s#</component># <pkgname>$(xml_escape "$pkgname")</pkgname>\n</component>#" "$destdir/$base"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
rm -rf "$extractroot"
|
||||||
|
fi
|
||||||
|
rm -f "$filelist"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$(find "$composeroot" -type f 2>/dev/null)" ]; then
|
||||||
|
log "Keine Desktop-/AppStream-Dateien in den Paketen gefunden und kein Paket in console_apps: - ueberspringe Katalog-Generierung."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
generate_fallback_metainfo "$composeroot" "$pkgmap"
|
||||||
|
|
||||||
|
rm -rf "$asout"
|
||||||
|
mkdir -p "$asout"
|
||||||
|
# Exit-Code NICHT sofort als fatal behandeln: appstreamcli compose liefert
|
||||||
|
# bei einzelnen fehlerhaften Komponenten ("Run failed, some data was
|
||||||
|
# ignored") einen Fehler-Exitcode, schreibt aber trotzdem einen Katalog
|
||||||
|
# mit allen UEBRIGEN, gueltigen Komponenten. Nur wenn am Ende wirklich
|
||||||
|
# gar kein Katalog entstanden ist, brechen wir ab.
|
||||||
|
if ! appstreamcli compose --prefix=/usr --origin="$id" --result-root="$asout" "$composeroot" \
|
||||||
|
>>"$CACHE/appstream-$id.log" 2>&1; then
|
||||||
|
log "WARNUNG: appstreamcli compose meldet Fehler bei einzelnen Komponenten, siehe $CACHE/appstream-$id.log - verwende ggf. trotzdem erzeugten Teil-Katalog."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local catalog
|
||||||
|
catalog=$(find "$asout" -type f \( -iname '*.xml.gz' -o -iname '*.yml.gz' \) | head -n1)
|
||||||
|
if [ -z "$catalog" ]; then
|
||||||
|
log "WARNUNG: appstreamcli compose hat keinen Katalog erzeugt, siehe $CACHE/appstream-$id.log"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `appstreamcli compose` schreibt den Origin nur in Datei- und
|
||||||
|
# Verzeichnisnamen, NICHT als origin=-Attribut in den Katalog. libappstream
|
||||||
|
# (AsPool) liest den Origin einer Katalogdatei aber genau aus diesem
|
||||||
|
# Attribut und findet ohne ihn die zugehoerigen Icons unter
|
||||||
|
# /var/cache/swcatalog/icons/<origin>/ nicht - die App erscheint in
|
||||||
|
# Discover dann ohne Symbol. Deshalb hier nachtragen.
|
||||||
|
if ! zcat "$catalog" | head -c 400 | grep -q 'origin='; then
|
||||||
|
if zcat "$catalog" | sed "0,/<components /s//<components origin=\"$id\" /" \
|
||||||
|
| gzip -9 > "$catalog.tmp"; then
|
||||||
|
mv "$catalog.tmp" "$catalog"
|
||||||
|
else
|
||||||
|
log "WARNUNG: konnte origin=\"$id\" nicht in $catalog eintragen - Icons fehlen ggf. in Discover."
|
||||||
|
rm -f "$catalog.tmp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --compress-type=gz ist PFLICHT: modifyrepo_c komprimiert die uebergebene
|
||||||
|
# Datei neu und nutzt auf AlmaLinux 10 per Default zstd. Das dort
|
||||||
|
# installierte AppStream 1.0.2 kennt in as_metadata_file_guess_style() aber
|
||||||
|
# nur .xml/.xml.gz/.yml/.yml.gz (.xml.zst erst ab AppStream 1.0.5). Bei
|
||||||
|
# einer .xml.zst lehnt as_utils_install_metadata_file() die Datei mit
|
||||||
|
# "Can not process files of this type" ab, PackageKit bricht daraufhin den
|
||||||
|
# gesamten Repo-Refresh ab und in /var/cache/swcatalog/ landet nie etwas -
|
||||||
|
# Discover und appstreamcli sehen das Repo dann gar nicht.
|
||||||
|
modifyrepo_c --compress-type=gz --mdtype=appstream "$catalog" "$out/repodata"
|
||||||
|
|
||||||
|
# Icons: as_utils_install_metadata_file() akzeptiert nur *.tar.gz UND
|
||||||
|
# verlangt eine der bekannten Groessen (48x48, 64x64, 128x128, jeweils
|
||||||
|
# optional @2) IM DATEINAMEN - fehlt sie, bricht es mit "Unable to find
|
||||||
|
# valid icon size in icon tarball name" ab und reisst wieder den ganzen
|
||||||
|
# Refresh mit. Entpackt wird flach nach
|
||||||
|
# /var/cache/swcatalog/icons/<repo-id>/<groesse>/, der Tarball darf also
|
||||||
|
# nur die PNGs auf oberster Ebene enthalten (nicht <origin>/<groesse>/...).
|
||||||
|
# PackageKit holt genau EINEN appstream-icons-Eintrag, wir liefern deshalb
|
||||||
|
# genau eine Groesse aus (64x64 ist das, was Discover/GNOME Software in der
|
||||||
|
# Listenansicht nutzen).
|
||||||
|
local icons_root icons_size icons_src tarball
|
||||||
|
icons_root=$(find "$asout" -type d -name 'icons' | head -n1)
|
||||||
|
for icons_size in 64x64 128x128 48x48; do
|
||||||
|
if [ -n "$icons_root" ] && [ -d "$icons_root/$id/$icons_size" ] \
|
||||||
|
&& [ -n "$(find "$icons_root/$id/$icons_size" -type f 2>/dev/null)" ]; then
|
||||||
|
icons_src="$icons_root/$id/$icons_size"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "${icons_src:-}" ]; then
|
||||||
|
tarball="$asout/$id-icons-$icons_size.tar.gz"
|
||||||
|
rm -f "$tarball"
|
||||||
|
if tar -C "$icons_src" -czf "$tarball" . ; then
|
||||||
|
modifyrepo_c --compress-type=gz --mdtype=appstream-icons "$tarball" "$out/repodata"
|
||||||
|
else
|
||||||
|
log "WARNUNG: Icon-Tarball fuer $id konnte nicht gepackt werden - Apps erscheinen in Discover ohne Symbol."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "WARNUNG: appstreamcli compose hat keine Icons erzeugt - Apps erscheinen in Discover ohne Symbol."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Holt + filtert + signiert die Pakete EINES Fremdrepos in einen privaten,
|
||||||
|
# nur intern genutzten Cache ($SIGNED_ROOT/$id) - noch NICHT im oeffentlichen
|
||||||
|
# Ausgabeverzeichnis. Der eigentliche Merge zu einem gemeinsamen Repo passiert
|
||||||
|
# erst danach, einmalig fuer alle Fremdrepos zusammen (siehe unten).
|
||||||
|
# Args: id baseurl gpgkey gpgcheck(0/1) paket1 [paket2 ...]
|
||||||
|
sync_one_repo() {
|
||||||
|
local id="$1" baseurl="$2" gpgkey="$3" gpgcheck="$4"
|
||||||
|
shift 4
|
||||||
|
local -a packages=("$@")
|
||||||
|
local stage="$CACHE/stage/$id"
|
||||||
|
local signed="$SIGNED_ROOT/$id"
|
||||||
|
local tmp_repofile="$CACHE/tmp-repos/$id.repo"
|
||||||
|
|
||||||
|
log "== $id: hole ${#packages[@]} Paket(e) aus $baseurl =="
|
||||||
|
rm -f "$CACHE/tmp-repos"/*.repo
|
||||||
|
{
|
||||||
|
echo "[$id]"
|
||||||
|
echo "name=$id"
|
||||||
|
echo "baseurl=$baseurl"
|
||||||
|
echo "enabled=1"
|
||||||
|
echo "gpgcheck=$gpgcheck"
|
||||||
|
[ -n "$gpgkey" ] && echo "gpgkey=$gpgkey"
|
||||||
|
} > "$tmp_repofile"
|
||||||
|
|
||||||
|
rm -rf "$stage"
|
||||||
|
mkdir -p "$stage"
|
||||||
|
if ! dnf --setopt="reposdir=$CACHE/tmp-repos" \
|
||||||
|
--disablerepo='*' --enablerepo="$id" \
|
||||||
|
--forcearch="$ALMA_BASEARCH" \
|
||||||
|
download --destdir="$stage" "${packages[@]}" \
|
||||||
|
>>"$CACHE/download-$id.log" 2>&1; then
|
||||||
|
log "FEHLER: dnf download fuer $id fehlgeschlagen, siehe $CACHE/download-$id.log - ueberspringe (bisheriger Stand von $id bleibt im gemeinsamen Repo erhalten)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "== $id: filtere Pakete, die Alma bereits bereitstellt =="
|
||||||
|
local removed=0 kept=0
|
||||||
|
while IFS= read -r -d '' rpm; do
|
||||||
|
local name
|
||||||
|
name=$(rpm -qp --qf '%{NAME}' "$rpm" 2>/dev/null)
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if grep -qxF "$name" "$CACHE/alma-names.txt" 2>/dev/null && ! is_force_included "$name"; then
|
||||||
|
rm -f "$rpm"
|
||||||
|
removed=$((removed + 1))
|
||||||
|
else
|
||||||
|
kept=$((kept + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$stage" -maxdepth 1 -name '*.rpm' -print0)
|
||||||
|
log "$id: $kept Pakete werden gespiegelt, $removed uebersprungen (bereits ueber Alma verfuegbar)."
|
||||||
|
|
||||||
|
log "== $id: aktualisiere internen Paketstand und signiere neue/geaenderte Pakete =="
|
||||||
|
mkdir -p "$signed"
|
||||||
|
local changed_list rsync_out
|
||||||
|
changed_list=$(mktemp)
|
||||||
|
rsync_out=$(mktemp)
|
||||||
|
if ! rsync -ai --delete --include='*.rpm' --exclude='*' "$stage"/ "$signed"/ > "$rsync_out" 2>&1; then
|
||||||
|
log "FEHLER: rsync (interner Paketstand) fuer $id fehlgeschlagen: $(cat "$rsync_out") - ueberspringe $id fuer diesen Lauf."
|
||||||
|
rm -f "$changed_list" "$rsync_out"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
awk '/^[><]f/{print $2}' "$rsync_out" > "$changed_list"
|
||||||
|
rm -f "$rsync_out"
|
||||||
|
|
||||||
|
local resign_failed=0
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -z "$f" ] && continue
|
||||||
|
if ! rpmsign --resign \
|
||||||
|
--define "_gpg_name $GPG_KEY_ID" \
|
||||||
|
--define "_gpg_path $GNUPGHOME" \
|
||||||
|
"$signed/$f" >>"$CACHE/rpmsign-$id.log" 2>&1; then
|
||||||
|
log "FEHLER: Signieren von $signed/$f fehlgeschlagen, siehe $CACHE/rpmsign-$id.log"
|
||||||
|
resign_failed=1
|
||||||
|
fi
|
||||||
|
done < "$changed_list"
|
||||||
|
local changed_count
|
||||||
|
changed_count=$(wc -l < "$changed_list")
|
||||||
|
rm -f "$changed_list"
|
||||||
|
log "$id: $changed_count Pakete neu signiert."
|
||||||
|
if [ "$resign_failed" -ne 0 ]; then
|
||||||
|
log "FEHLER: mind. ein Paket in $id konnte nicht signiert werden - $id wird bei der Zusammenfuehrung uebersprungen."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "== $id: fertig =="
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! fetch_packages_yaml; then
|
||||||
|
log "FEHLER: keine packages.yaml verfuegbar, breche Sync-Lauf komplett ab."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t FORCE_INCLUDE_NAMES < <(jq -r '.force_include[]? // empty' "$PACKAGES_JSON")
|
||||||
|
mapfile -t CONSOLE_APP_NAMES < <(jq -r '.console_apps[]? // empty' "$PACKAGES_JSON")
|
||||||
|
|
||||||
|
refresh_alma_names
|
||||||
|
if [ ! -s "$CACHE/alma-names.txt" ]; then
|
||||||
|
log "FEHLER: keine Alma-Baseline-Namen verfuegbar, breche Sync-Lauf ab (Sicherheitsnetz gegen versehentliches Doppel-Spiegeln)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
overall_status=0
|
||||||
|
all_ids=()
|
||||||
|
repo_count=$(jq '.repos | length' "$PACKAGES_JSON")
|
||||||
|
for ((i = 0; i < repo_count; i++)); do
|
||||||
|
repo_json=$(jq -c ".repos[$i]" "$PACKAGES_JSON")
|
||||||
|
id=$(jq -r '.id // empty' <<<"$repo_json")
|
||||||
|
baseurl=$(jq -r '.baseurl // empty' <<<"$repo_json")
|
||||||
|
gpgkey=$(jq -r '.gpgkey // empty' <<<"$repo_json")
|
||||||
|
gpgcheck=$(jq -r 'if .gpgcheck == false then "0" else "1" end' <<<"$repo_json")
|
||||||
|
mapfile -t packages < <(jq -r '.packages[]? // empty' <<<"$repo_json")
|
||||||
|
|
||||||
|
if [ -z "$id" ] || [ -z "$baseurl" ] || [ "${#packages[@]}" -eq 0 ]; then
|
||||||
|
log "WARNUNG: Eintrag Nr. $((i + 1)) in packages.yaml unvollstaendig (id/baseurl/packages fehlt), ueberspringe."
|
||||||
|
overall_status=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
all_ids+=("$id")
|
||||||
|
sync_one_repo "$id" "$baseurl" "$gpgkey" "$gpgcheck" "${packages[@]}" || overall_status=1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Internen Paketstand von Fremdrepos aufraeumen, die nicht mehr in
|
||||||
|
# packages.yaml stehen - sonst wuerden deren alte Pakete beim Merge unten
|
||||||
|
# unnoetig im Cache liegen bleiben (sie werden ja schon jetzt nicht mehr in
|
||||||
|
# die Merge-Quellenliste aufgenommen, siehe unten - das hier ist nur Hygiene).
|
||||||
|
if [ -d "$SIGNED_ROOT" ]; then
|
||||||
|
for d in "$SIGNED_ROOT"/*/; do
|
||||||
|
[ -d "$d" ] || continue
|
||||||
|
existing_id=$(basename "$d")
|
||||||
|
keep=0
|
||||||
|
for id in "${all_ids[@]}"; do
|
||||||
|
[ "$id" = "$existing_id" ] && keep=1 && break
|
||||||
|
done
|
||||||
|
if [ "$keep" -eq 0 ]; then
|
||||||
|
log "Entferne internen Paketstand des nicht mehr in packages.yaml gelisteten Fremdrepos '$existing_id' ..."
|
||||||
|
rm -rf "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "== Fuehre alle Fremdrepos zu EINEM gemeinsamen Ausgabe-Repo zusammen =="
|
||||||
|
merge_sources=()
|
||||||
|
for id in "${all_ids[@]}"; do
|
||||||
|
[ -d "$SIGNED_ROOT/$id" ] && merge_sources+=("$SIGNED_ROOT/$id/")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#merge_sources[@]}" -eq 0 ]; then
|
||||||
|
log "FEHLER: kein Fremdrepo hat je erfolgreich Pakete geliefert - gemeinsames Repo bleibt unveraendert/leer."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! rsync -ai --delete --include='*.rpm' --exclude='*' "${merge_sources[@]}" "$OUT"/ >>"$CACHE/merge.log" 2>&1; then
|
||||||
|
log "FEHLER: Zusammenfuehren der Fremdrepos zum gemeinsamen Repo fehlgeschlagen, siehe $CACHE/merge.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "== baue Repo-Metadaten fuer das gemeinsame Repo =="
|
||||||
|
if ! createrepo_c --update "$OUT" >>"$CACHE/createrepo.log" 2>&1; then
|
||||||
|
log "FEHLER: createrepo_c fehlgeschlagen, siehe $CACHE/createrepo.log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "== baue AppStream-Metadaten (fuer Discover/GNOME Software) =="
|
||||||
|
if command -v appstreamcli >/dev/null 2>&1; then
|
||||||
|
generate_appstream_via_compose "$MIRROR_REPO_ID" "$OUT"
|
||||||
|
else
|
||||||
|
log "WARNUNG: appstreamcli nicht installiert - Pakete erscheinen ohne App-Katalogeintrag in Discover."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "== signiere repomd.xml =="
|
||||||
|
gpg --batch --yes --detach-sign --armor --local-user "$GPG_KEY_ID" \
|
||||||
|
--output "$OUT/repodata/repomd.xml.asc" "$OUT/repodata/repomd.xml"
|
||||||
|
|
||||||
|
cat > "$OUT/$MIRROR_REPO_ID.repo" <<EOF
|
||||||
|
[$MIRROR_REPO_ID]
|
||||||
|
name=$MIRROR_REPO_ID (Mirror)
|
||||||
|
baseurl=https://$MIRROR_DOMAIN/dnf/
|
||||||
|
enabled=1
|
||||||
|
gpgcheck=1
|
||||||
|
repo_gpgcheck=1
|
||||||
|
gpgkey=https://$MIRROR_DOMAIN/dnf/RPM-GPG-KEY-mirror
|
||||||
|
EOF
|
||||||
|
|
||||||
|
package_count=$(find "$OUT" -maxdepth 1 -name '*.rpm' | wc -l)
|
||||||
|
log "== fertig: $package_count Paket(e) im gemeinsamen Repo =="
|
||||||
|
|
||||||
|
exit "$overall_status"
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Gemeinsamer Stack fuer beide Spiegel. Beide Sync-Container schreiben nur in
|
||||||
|
# ihr eigenes Volume; EIN nginx liefert beides unter einer Domain aus:
|
||||||
|
# https://<MIRROR_DOMAIN>/flatpak/ -> Flathub-Filter-Spiegel (OSTree)
|
||||||
|
# https://<MIRROR_DOMAIN>/dnf/ -> RPM-Spiegel (createrepo_c)
|
||||||
|
#
|
||||||
|
# Beide Container bekommen denselben, EXTERN erzeugten Signing-Key ueber das
|
||||||
|
# gemeinsame Verzeichnis ./gpg (read-only) herein und beide holen ihre
|
||||||
|
# Freigabeliste aus demselben Git-Repo (SOURCES_GIT_URL): flatpaks.yaml fuer
|
||||||
|
# den Flatpak-Spiegel, packages.yaml fuer den DNF-Spiegel.
|
||||||
|
services:
|
||||||
|
flatpak-mirror:
|
||||||
|
build: ./flatpak-mirror
|
||||||
|
container_name: flatpak-mirror
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MIRROR_DOMAIN: ${MIRROR_DOMAIN}
|
||||||
|
MIRROR_REPO_ID: ${MIRROR_REPO_ID:-company-mirror}
|
||||||
|
GPG_KEY_ID: ${GPG_KEY_ID}
|
||||||
|
SYNC_INTERVAL_SECONDS: ${FLATPAK_SYNC_INTERVAL_SECONDS:-900}
|
||||||
|
SOURCES_GIT_URL: ${SOURCES_GIT_URL}
|
||||||
|
SOURCES_GIT_REF: ${SOURCES_GIT_REF:-main}
|
||||||
|
SOURCES_GIT_USERNAME: ${SOURCES_GIT_USERNAME:-}
|
||||||
|
SOURCES_GIT_TOKEN: ${SOURCES_GIT_TOKEN:-}
|
||||||
|
SOURCES_GIT_SSH_KEY: ${SOURCES_GIT_SSH_KEY:-}
|
||||||
|
FLATHUB_REMOTE_URL: ${FLATHUB_REMOTE_URL:-https://dl.flathub.org/repo/}
|
||||||
|
FLATHUB_GPG_URL: ${FLATHUB_GPG_URL:-https://dl.flathub.org/repo/flathub.gpg}
|
||||||
|
volumes:
|
||||||
|
- ./gpg:/run/secrets/gpg:ro
|
||||||
|
- ./git-ssh:/run/secrets/git-ssh:ro
|
||||||
|
- flatpak-repo-data:/srv/flatpak
|
||||||
|
- flatpak-cache:/var/cache/flatpak-mirror
|
||||||
|
|
||||||
|
dnf-mirror:
|
||||||
|
build: ./dnf-mirror
|
||||||
|
container_name: dnf-mirror
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MIRROR_DOMAIN: ${MIRROR_DOMAIN}
|
||||||
|
MIRROR_REPO_ID: ${MIRROR_REPO_ID:-company-mirror}
|
||||||
|
GPG_KEY_ID: ${GPG_KEY_ID}
|
||||||
|
SYNC_INTERVAL_SECONDS: ${DNF_SYNC_INTERVAL_SECONDS:-21600}
|
||||||
|
SOURCES_GIT_URL: ${SOURCES_GIT_URL}
|
||||||
|
SOURCES_GIT_REF: ${SOURCES_GIT_REF:-main}
|
||||||
|
SOURCES_GIT_USERNAME: ${SOURCES_GIT_USERNAME:-}
|
||||||
|
SOURCES_GIT_TOKEN: ${SOURCES_GIT_TOKEN:-}
|
||||||
|
SOURCES_GIT_SSH_KEY: ${SOURCES_GIT_SSH_KEY:-}
|
||||||
|
ALMA_RELEASEVER: ${ALMA_RELEASEVER:-10}
|
||||||
|
ALMA_BASEARCH: ${ALMA_BASEARCH:-x86_64}
|
||||||
|
ALMA_BASELINE_REPOIDS: ${ALMA_BASELINE_REPOIDS:-baseos,appstream,extras,crb,epel}
|
||||||
|
ALMA_BASELINE_REPOSDIR: ${ALMA_BASELINE_REPOSDIR:-}
|
||||||
|
volumes:
|
||||||
|
- ./gpg:/run/secrets/gpg:ro
|
||||||
|
- ./git-ssh:/run/secrets/git-ssh:ro
|
||||||
|
- ./dnf-mirror/alma-baseline.repo.d:/etc/mirror/alma-baseline.repo.d:ro
|
||||||
|
- dnf-repo-data:/srv/dnf
|
||||||
|
- dnf-cache:/var/cache/mirror
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: mirror-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- flatpak-mirror
|
||||||
|
- dnf-mirror
|
||||||
|
ports:
|
||||||
|
- "${HTTP_PORT:-80}:80"
|
||||||
|
volumes:
|
||||||
|
- flatpak-repo-data:/usr/share/nginx/html/flatpak:ro
|
||||||
|
- dnf-repo-data:/usr/share/nginx/html/dnf:ro
|
||||||
|
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
flatpak-repo-data:
|
||||||
|
flatpak-cache:
|
||||||
|
dnf-repo-data:
|
||||||
|
dnf-cache:
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM debian:trixie-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
ostree \
|
||||||
|
flatpak \
|
||||||
|
git \
|
||||||
|
openssh-client \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# yq (mikefarah, Go-Binary) zum Parsen von flatpaks.yaml
|
||||||
|
RUN curl -sSL -o /usr/local/bin/yq \
|
||||||
|
https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
|
||||||
|
&& chmod +x /usr/local/bin/yq
|
||||||
|
|
||||||
|
# Gemeinsamer Signing-Key: entrypoint.sh kopiert ihn beim Start aus dem
|
||||||
|
# read-only gemounteten /run/secrets/gpg hierher.
|
||||||
|
ENV GNUPGHOME=/etc/mirror-gpg
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY gpg-import.sh /usr/local/lib/gpg-import.sh
|
||||||
|
COPY entrypoint.sh /app/entrypoint.sh
|
||||||
|
COPY sync.sh /app/sync.sh
|
||||||
|
RUN chmod +x /app/entrypoint.sh /app/sync.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Uebernimmt den gemeinsamen, EXTERN erzeugten Signing-Key aus dem read-only
|
||||||
|
# gemounteten Verzeichnis /run/secrets/gpg (auf dem Host: ./gpg neben der
|
||||||
|
# docker-compose.yml) in ein containerlokales GNUPGHOME.
|
||||||
|
#
|
||||||
|
# Bewusst eine Kopie statt direkt darauf zu arbeiten: gpg schreibt in seinem
|
||||||
|
# Homedir (trustdb, random_seed, Lockfiles), und derselbe Ordner ist
|
||||||
|
# gleichzeitig im dnf-mirror-Container gemountet. Die Kopie haelt das
|
||||||
|
# Original unveraendert und vermeidet, dass sich beide Container beim
|
||||||
|
# Signieren gegenseitig ins Gehege kommen.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GPG_SOURCE=/run/secrets/gpg
|
||||||
|
export GNUPGHOME="${GNUPGHOME:-/etc/mirror-gpg}"
|
||||||
|
|
||||||
|
: "${GPG_KEY_ID:?GPG_KEY_ID muss gesetzt sein (gemeinsamer Signing-Key, siehe .env)}"
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
. /usr/local/lib/gpg-import.sh
|
||||||
|
|
||||||
|
import_shared_gpg
|
||||||
|
require_usable_signing_key
|
||||||
|
|
||||||
|
exec /app/sync.sh
|
||||||
Executable
+95
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Gemeinsame Hilfsfunktionen beider Spiegel-Container: den EXTERN erzeugten
|
||||||
|
# Signing-Key aus dem read-only gemounteten $GPG_SOURCE uebernehmen und
|
||||||
|
# pruefen, ob damit unbeaufsichtigt signiert werden kann.
|
||||||
|
#
|
||||||
|
# ACHTUNG: identische Kopie in dnf-mirror/gpg-import.sh - Aenderungen bitte in
|
||||||
|
# beiden pflegen. (Getrennte Build-Kontexte je Image; ein gemeinsamer Kontext
|
||||||
|
# wuerde das Schluesselverzeichnis ./gpg in den Build-Kontext ziehen.)
|
||||||
|
|
||||||
|
# Die Dateien, die ein GnuPG-Homedir ausmachen. Bewusst eine feste Liste statt
|
||||||
|
# "cp -a .": im Quellverzeichnis liegen oft noch die Agent-Sockets
|
||||||
|
# (S.gpg-agent & Co.) und Lock-Dateien eines gpg-agent, der auf dem HOST
|
||||||
|
# lief/laeuft. Die haben im Container nichts verloren.
|
||||||
|
GPG_HOME_ENTRIES=(pubring.kbx pubring.gpg secring.gpg trustdb.gpg \
|
||||||
|
private-keys-v1.d openpgp-revocs.d public-keys.d)
|
||||||
|
|
||||||
|
import_shared_gpg() {
|
||||||
|
if [ ! -d "$GPG_SOURCE" ]; then
|
||||||
|
echo "[gpg] FEHLER: $GPG_SOURCE ist nicht gemountet - fehlt der Ordner ./gpg?" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$GNUPGHOME"
|
||||||
|
mkdir -p "$GNUPGHOME"
|
||||||
|
chmod 700 "$GNUPGHOME"
|
||||||
|
|
||||||
|
local entry copied=0
|
||||||
|
for entry in "${GPG_HOME_ENTRIES[@]}"; do
|
||||||
|
[ -e "$GPG_SOURCE/$entry" ] || continue
|
||||||
|
# Kein "|| true": schlaegt das Kopieren des Schluesselmaterials fehl,
|
||||||
|
# soll das laut und sofort auffallen statt spaeter als "Key nicht
|
||||||
|
# gefunden".
|
||||||
|
cp -a "$GPG_SOURCE/$entry" "$GNUPGHOME/"
|
||||||
|
copied=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$copied" -eq 0 ]; then
|
||||||
|
echo "[gpg] FEHLER: in $GPG_SOURCE liegt kein GnuPG-Schluesselbund." >&2
|
||||||
|
echo "[gpg] Erwartet wird der INHALT eines GnuPG-Homedirs (pubring.kbx," >&2
|
||||||
|
echo "[gpg] private-keys-v1.d, ...), nicht z.B. eine exportierte .asc-Datei." >&2
|
||||||
|
echo "[gpg] Tatsaechlicher Inhalt:" >&2
|
||||||
|
ls -la "$GPG_SOURCE" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod -R go-rwx "$GNUPGHOME"
|
||||||
|
|
||||||
|
# Signiert wird unbeaufsichtigt, also nie ein Pinentry-Fenster erwarten.
|
||||||
|
echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf"
|
||||||
|
echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Listet die im Homedir tatsaechlich vorhandenen geheimen Schluessel als
|
||||||
|
# "<fingerprint> <uid>" - genau das, was als GPG_KEY_ID in die .env gehoert.
|
||||||
|
list_available_secret_keys() {
|
||||||
|
gpg --batch --list-secret-keys --with-colons 2>/dev/null | awk -F: '
|
||||||
|
$1 == "sec" { want = 1; fpr = ""; next }
|
||||||
|
$1 == "fpr" && want { fpr = $10; want = 0; next }
|
||||||
|
$1 == "uid" && fpr != "" { print " " fpr " " $10; fpr = ""; next }
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prueft nicht nur, OB der Key da ist, sondern ob damit auch wirklich
|
||||||
|
# unbeaufsichtigt signiert werden kann - ein passphrasegeschuetzter Key faellt
|
||||||
|
# sonst erst mitten im ersten Sync-Lauf auf.
|
||||||
|
require_usable_signing_key() {
|
||||||
|
if ! gpg --batch --list-secret-keys "$GPG_KEY_ID" >/dev/null 2>&1; then
|
||||||
|
echo "[gpg] FEHLER: kein geheimer Schluessel '$GPG_KEY_ID' in ./gpg gefunden." >&2
|
||||||
|
local available
|
||||||
|
available="$(list_available_secret_keys)"
|
||||||
|
if [ -n "$available" ]; then
|
||||||
|
echo "[gpg] Vorhanden sind:" >&2
|
||||||
|
echo "$available" >&2
|
||||||
|
echo "[gpg] Einen dieser Fingerprints als GPG_KEY_ID in die .env eintragen." >&2
|
||||||
|
else
|
||||||
|
echo "[gpg] Der Schluesselbund enthaelt ueberhaupt keinen GEHEIMEN Schluessel" >&2
|
||||||
|
echo "[gpg] (nur oeffentliche?). private-keys-v1.d/ pruefen - dort muss" >&2
|
||||||
|
echo "[gpg] mindestens eine .key-Datei liegen." >&2
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local err
|
||||||
|
err="$(printf 'signing-selftest' \
|
||||||
|
| gpg --batch --yes --local-user "$GPG_KEY_ID" --detach-sign --output /dev/null - 2>&1)" && {
|
||||||
|
echo "[gpg] Gemeinsamen Signing-Key '$GPG_KEY_ID' uebernommen, Testsignatur ok."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "[gpg] FEHLER: Schluessel '$GPG_KEY_ID' ist da, aber Signieren schlaegt fehl:" >&2
|
||||||
|
echo "$err" >&2
|
||||||
|
echo "[gpg] Haeufigste Ursache: der Key hat eine Passphrase. Unbeaufsichtigtes" >&2
|
||||||
|
echo "[gpg] Signieren braucht einen Key OHNE Passphrase (siehe README)." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
Executable
+348
@@ -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
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Ein Server-Block fuer beide Spiegel. Die Ausgabe-Volumes der beiden
|
||||||
|
# Sync-Container sind read-only unter /usr/share/nginx/html/{flatpak,dnf}
|
||||||
|
# gemountet (siehe docker-compose.yml), die URL-Pfade entsprechen also 1:1
|
||||||
|
# den Verzeichnisnamen.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
|
||||||
|
# Flatpak/OSTree macht sehr viele kleine Einzel-Requests (archive-z2).
|
||||||
|
# Keep-Alive ist deshalb wichtig fuer akzeptable Performance.
|
||||||
|
keepalive_timeout 65;
|
||||||
|
keepalive_requests 1000;
|
||||||
|
|
||||||
|
# Grosse Objekte/Static-Deltas und RPMs effizient ausliefern.
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
default_type text/plain;
|
||||||
|
return 200 "flatpak: /flatpak/\ndnf: /dnf/\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Flatpak-Spiegel (OSTree-Repo, Volume flatpak-repo-data) -----------
|
||||||
|
location /flatpak/ {
|
||||||
|
autoindex off;
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# summary/summary.sig sind das Einzige, was flatpak bei jedem Zugriff neu
|
||||||
|
# holt - nicht cachen, sonst sehen Clients neue Apps erst verzoegert.
|
||||||
|
# Objekte darunter sind content-addressed und damit unveraenderlich.
|
||||||
|
location ~* ^/flatpak/summary(\.sig|\.idx)?$ {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* ^/flatpak/(objects|deltas)/ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- DNF-Spiegel (Volume dnf-repo-data) --------------------------------
|
||||||
|
location /dnf/ {
|
||||||
|
autoindex on;
|
||||||
|
}
|
||||||
|
|
||||||
|
# repomd.xml (und dessen Signatur) ist der einzige Teil, den dnf bei jedem
|
||||||
|
# Zugriff neu abruft - nicht cachen. Regex-Location gewinnt gegenueber der
|
||||||
|
# Praefix-Location /dnf/ oben (nginx-Standardverhalten). Ein gemeinsames
|
||||||
|
# Repo -> repodata liegt direkt unter /dnf/.
|
||||||
|
location ~* ^/dnf/repodata/repomd\.xml(\.asc)?$ {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Vorlage: gemeinsames Freigabe-Repo
|
||||||
|
|
||||||
|
Beide Spiegel holen ihre Freigabeliste aus **einem** Git-Repo
|
||||||
|
(`SOURCES_GIT_URL` in der `.env`). Der Inhalt dieses Ordners gehoert dorthin,
|
||||||
|
ins Wurzelverzeichnis:
|
||||||
|
|
||||||
|
```
|
||||||
|
flatpaks.yaml Freigabeliste des Flatpak-Spiegels (Flathub-App-IDs)
|
||||||
|
packages.yaml Freigabeliste des DNF-Spiegels (Fremdrepos + Paketnamen)
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide Container fetchen dieses Repo vor **jedem** Sync-Lauf. Aenderungen an
|
||||||
|
den Listen wirken also spaetestens beim naechsten automatischen Lauf, ohne
|
||||||
|
Zugriff auf den Docker-Host und ohne Neubau/Neustart der Container - die
|
||||||
|
Pflege der Freigaben laesst sich damit per Merge-Request an ein anderes Team
|
||||||
|
delegieren.
|
||||||
|
|
||||||
|
Der Zugriff braucht nur Lesen: Deploy-Key (SSH) oder Read-only-Token (HTTPS),
|
||||||
|
siehe `SOURCES_GIT_*` in der `.env.example`.
|
||||||
|
|
||||||
|
Fehlt eine der beiden Dateien, bricht nur der jeweilige Spiegel ab - der
|
||||||
|
andere laeuft normal weiter.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Freigabeliste des FLATPAK-Spiegels.
|
||||||
|
#
|
||||||
|
# Diese Datei lebt im SEPARATEN Git-Repo, das ueber SOURCES_GIT_URL/
|
||||||
|
# SOURCES_GIT_REF eingebunden wird - im selben Repo wie packages.yaml. Der
|
||||||
|
# flatpak-mirror-Container liest sie vor jedem Sync-Lauf frisch ein.
|
||||||
|
#
|
||||||
|
# Gespiegelt wird genau das, was hier steht, plus die Runtimes, die die Apps
|
||||||
|
# laut ihrer eigenen Metadaten brauchen (die loest der Sync rekursiv selbst
|
||||||
|
# auf). Ein hier entfernter Eintrag verschwindet beim naechsten Sync-Lauf
|
||||||
|
# auch aus dem Spiegel.
|
||||||
|
#
|
||||||
|
# id Application-ID auf Flathub (z.B. org.mozilla.firefox)
|
||||||
|
# branch Flathub-Branch, praktisch immer "stable"
|
||||||
|
# reason frei, nur zur Dokumentation der Freigabe
|
||||||
|
|
||||||
|
apps:
|
||||||
|
- id: org.mozilla.firefox
|
||||||
|
branch: stable
|
||||||
|
reason: "Standard-Browser, freigegeben 2026-08"
|
||||||
|
|
||||||
|
- id: org.gimp.GIMP
|
||||||
|
branch: stable
|
||||||
|
reason: "Grafikbearbeitung, Marketing-Team"
|
||||||
|
|
||||||
|
- id: org.libreoffice.LibreOffice
|
||||||
|
branch: stable
|
||||||
|
reason: "Office-Suite, Standard-Ausstattung"
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# Diese Datei lebt im SEPARATEN Git-Repo, das ueber SOURCES_GIT_URL/
|
||||||
|
# SOURCES_GIT_REF eingebunden wird (siehe README.md in diesem Ordner). Der
|
||||||
|
# dnf-mirror-Container liest sie vor jedem Sync-Lauf frisch ein.
|
||||||
|
#
|
||||||
|
# Pro Fremdrepo (repos[]) werden NUR die hier explizit gelisteten
|
||||||
|
# Paketnamen geholt (dnf download) - keine automatische Aufloesung des
|
||||||
|
# gesamten Repo-Inhalts und KEINE automatische Abhaengigkeitsaufloesung.
|
||||||
|
# Braucht eine Anwendung eine Bibliothek, die weder in AlmaLinux/EPEL noch im
|
||||||
|
# selben Fremdrepo steckt, muss sie als eigener Eintrag (ggf. mit eigenem
|
||||||
|
# Fremdrepo) hier mit aufgenommen werden.
|
||||||
|
#
|
||||||
|
# Egal was hier gelistet ist: Pakete, deren NAME bereits ueber AlmaLinux/EPEL
|
||||||
|
# aufloesbar ist (siehe ALMA_BASELINE_REPOIDS), werden trotzdem automatisch
|
||||||
|
# uebersprungen - siehe force_include weiter unten fuer gezielte Ausnahmen.
|
||||||
|
|
||||||
|
# Paketnamen, die IMMER gespiegelt werden sollen, auch wenn AlmaLinux/EPEL
|
||||||
|
# ein gleichnamiges Paket hat (z.B. weil eine Anwendung eine neuere Version
|
||||||
|
# einer Bibliothek braucht, als Alma mitbringt).
|
||||||
|
#force_include: []
|
||||||
|
|
||||||
|
force_include:
|
||||||
|
- dotnet-sdk-8.0
|
||||||
|
- dotnet-sdk-9.0
|
||||||
|
- dotnet-sdk-10.0
|
||||||
|
|
||||||
|
console_apps:
|
||||||
|
- dotnet-sdk-8.0
|
||||||
|
- dotnet-sdk-9.0
|
||||||
|
- dotnet-sdk-10.0
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- id: xpipe
|
||||||
|
baseurl: https://rpm.xpipe.io
|
||||||
|
gpgkey: https://xpipe.io/signatures/0xDD3E0AD0.asc
|
||||||
|
packages:
|
||||||
|
- xpipe
|
||||||
|
|
||||||
|
- id: dotnet
|
||||||
|
baseurl: https://repo.almalinux.org/almalinux/10/AppStream/x86_64/os/
|
||||||
|
gpgkey: https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-10
|
||||||
|
packages:
|
||||||
|
- dotnet-sdk-8.0
|
||||||
|
- dotnet-sdk-9.0
|
||||||
|
- dotnet-sdk-10.0
|
||||||
Reference in New Issue
Block a user