From d83087cbbd7c02b42de17fe7b010d79be94fa71d Mon Sep 17 00:00:00 2001 From: leander19961 Date: Mon, 7 Sep 2026 11:55:21 +0200 Subject: [PATCH] Initial commit --- .env.example | 81 +++ .gitignore | 6 + README.md | 239 +++++++ dnf-mirror/Dockerfile | 58 ++ .../almalinux-baseline.repo | 44 ++ dnf-mirror/entrypoint.sh | 27 + dnf-mirror/gpg-import.sh | 95 +++ dnf-mirror/run-as-mirror.sh | 33 + dnf-mirror/sync.sh | 661 ++++++++++++++++++ docker-compose.yml | 76 ++ flatpak-mirror/Dockerfile | 28 + flatpak-mirror/entrypoint.sh | 24 + flatpak-mirror/gpg-import.sh | 95 +++ flatpak-mirror/sync.sh | 348 +++++++++ git-ssh/.gitkeep | 0 gpg/.gitkeep | 0 nginx/conf.d/repo.conf | 55 ++ sources-repo-template/README.md | 22 + sources-repo-template/flatpaks.yaml | 27 + sources-repo-template/packages.yaml | 44 ++ 20 files changed, 1963 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dnf-mirror/Dockerfile create mode 100644 dnf-mirror/alma-baseline.repo.d/almalinux-baseline.repo create mode 100755 dnf-mirror/entrypoint.sh create mode 100755 dnf-mirror/gpg-import.sh create mode 100755 dnf-mirror/run-as-mirror.sh create mode 100755 dnf-mirror/sync.sh create mode 100644 docker-compose.yml create mode 100644 flatpak-mirror/Dockerfile create mode 100755 flatpak-mirror/entrypoint.sh create mode 100755 flatpak-mirror/gpg-import.sh create mode 100755 flatpak-mirror/sync.sh create mode 100644 git-ssh/.gitkeep create mode 100644 gpg/.gitkeep create mode 100644 nginx/conf.d/repo.conf create mode 100644 sources-repo-template/README.md create mode 100644 sources-repo-template/flatpaks.yaml create mode 100644 sources-repo-template/packages.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4ec231c --- /dev/null +++ b/.env.example @@ -0,0 +1,81 @@ +# --------------------------------------------------------------------------- +# Gemeinsam fuer beide Spiegel +# --------------------------------------------------------------------------- +# Domain, unter der BEIDE Repos erreichbar sind: +# https:///flatpak/ und https:///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/.repo (dnf) +# /flatpak/.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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22e53c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Zugangsdaten und Schluesselmaterial gehoeren nie ins Git-Repo. +.env +gpg/* +!gpg/.gitkeep +git-ssh/* +!git-ssh/.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c6f929 --- /dev/null +++ b/README.md @@ -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:///flatpak/` | gefilterter Flathub-Spiegel (OSTree) | `flatpak-mirror` | +| `https:///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 ' 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:///flatpak/.flatpakrepo` | fertige Remote-Definition inkl. Key | +| `https:///flatpak/.gpg` | Public Key (binaer, fuer OSTree) | +| `https:///dnf/.repo` | die eine `.repo`-Datei fuer Clients | +| `https:///dnf/RPM-GPG-KEY-mirror` | Public Key (ASCII-armored, fuer RPM) | + +## Clients anbinden + +```sh +# Flatpak +flatpak remote-add --if-not-exists company-mirror \ + https:///flatpak/company-mirror.flatpakrepo + +# DNF +rpm --import https:///dnf/RPM-GPG-KEY-mirror +curl -o /etc/yum.repos.d/company-mirror.repo \ + https:///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//x86_64/` 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 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. diff --git a/dnf-mirror/Dockerfile b/dnf-mirror/Dockerfile new file mode 100644 index 0000000..b172a98 --- /dev/null +++ b/dnf-mirror/Dockerfile @@ -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"] diff --git a/dnf-mirror/alma-baseline.repo.d/almalinux-baseline.repo b/dnf-mirror/alma-baseline.repo.d/almalinux-baseline.repo new file mode 100644 index 0000000..e58021c --- /dev/null +++ b/dnf-mirror/alma-baseline.repo.d/almalinux-baseline.repo @@ -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 diff --git a/dnf-mirror/entrypoint.sh b/dnf-mirror/entrypoint.sh new file mode 100755 index 0000000..04559b7 --- /dev/null +++ b/dnf-mirror/entrypoint.sh @@ -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 diff --git a/dnf-mirror/gpg-import.sh b/dnf-mirror/gpg-import.sh new file mode 100755 index 0000000..d5e9a63 --- /dev/null +++ b/dnf-mirror/gpg-import.sh @@ -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 +# " " - 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 +} diff --git a/dnf-mirror/run-as-mirror.sh b/dnf-mirror/run-as-mirror.sh new file mode 100755 index 0000000..6f41a28 --- /dev/null +++ b/dnf-mirror/run-as-mirror.sh @@ -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 diff --git a/dnf-mirror/sync.sh b/dnf-mirror/sync.sh new file mode 100755 index 0000000..dae912a --- /dev/null +++ b/dnf-mirror/sync.sh @@ -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///\>}" + 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 '' + echo '' + echo " $(xml_escape "$pkgname")" + echo " $(xml_escape "$pkgname")" + echo ' CC0-1.0' + echo " $(xml_escape "$license")" + echo " $(xml_escape "$pkgname")" + echo " $(xml_escape "$summary")" + echo ' ' + # %{DESCRIPTION} ist Fliesstext mit Leerzeilen als Absatztrennern - + # AppStream verlangt hier

-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 "

$(xml_escape "$para")

" + done + echo '
' + [ -n "$url" ] && echo " $(xml_escape "$url")" + + # 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 ' ' + for binary in "${binaries[@]}"; do + echo " $(xml_escape "$binary")" + done + echo ' ' + fi + echo '
' + } > "$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 '' + echo '' + echo " ${base}" + echo " $(xml_escape "$pkgname")" + echo ' CC0-1.0' + echo ' LicenseRef-proprietary' + echo " $(xml_escape "$name")" + echo " $(xml_escape "$summary")" + echo " " + echo "

$(xml_escape "$summary")

" + echo "
" + echo " ${base}" + if [ -n "$categories" ]; then + echo ' ' + local IFS=';' + for cat in $categories; do + [ -z "$cat" ] && continue + echo " $(xml_escape "$cat")" + done + echo ' ' + fi + echo '
' + } > "$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//.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 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 (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 + elif ! grep -q '' "$destdir/$base" 2>/dev/null; then + sed -i "s## $(xml_escape "$pkgname")\n#" "$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// 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,/ "$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///, der Tarball darf also + # nur die PNGs auf oberster Ebene enthalten (nicht //...). + # 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" </flatpak/ -> Flathub-Filter-Spiegel (OSTree) +# https:///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: diff --git a/flatpak-mirror/Dockerfile b/flatpak-mirror/Dockerfile new file mode 100644 index 0000000..455cfa4 --- /dev/null +++ b/flatpak-mirror/Dockerfile @@ -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"] diff --git a/flatpak-mirror/entrypoint.sh b/flatpak-mirror/entrypoint.sh new file mode 100755 index 0000000..05aac90 --- /dev/null +++ b/flatpak-mirror/entrypoint.sh @@ -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 diff --git a/flatpak-mirror/gpg-import.sh b/flatpak-mirror/gpg-import.sh new file mode 100755 index 0000000..d5e9a63 --- /dev/null +++ b/flatpak-mirror/gpg-import.sh @@ -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 +# " " - 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 +} diff --git a/flatpak-mirror/sync.sh b/flatpak-mirror/sync.sh new file mode 100755 index 0000000..88e23b4 --- /dev/null +++ b/flatpak-mirror/sync.sh @@ -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// ---------- +resolve_pulled_checksum() { + local ref="$1" + if [ -f "$REPO_PATH/refs/heads/$ref" ]; then + cat "$REPO_PATH/refs/heads/$ref" + return 0 + fi + find "$REPO_PATH/refs/mirrors" -type f -path "*/$ref" 2>/dev/null | head -n1 | xargs -r cat +} + +# --- 4. Aktuell lokal verwaltete Refs ermitteln (Apps + Runtimes) ---------- +current_refs() { + ostree --repo="$REPO_PATH" refs | grep -E "^(app|runtime)/" || true +} + +# --- 5. Pull + rekursive Abhaengigkeitsaufloesung -------------------------- +# ACHTUNG: run_once() wird in der Hauptschleife als if-Bedingung aufgerufen. +# Damit ist "set -e" fuer den gesamten Aufrufbaum darunter ausgehebelt (Bash +# unterdrueckt den Abbruch in jedem Kontext, in dem der Rueckgabewert +# ausgewertet wird - und zwar rekursiv). Fehler muessen hier deshalb +# ausdruecklich geprueft und weitergereicht werden, sonst laeuft der Sync +# stillschweigend weiter. + +# Pullt einen einzelnen Ref und legt den passenden heads/-Ref an +# (siehe Kommentar bei resolve_pulled_checksum weiter oben). +pull_one_ref() { + local ref="$1" + log "Pull: ${ref}" + if ! ostree --repo="$REPO_PATH" pull --mirror flathub "$ref"; then + log "FEHLER: Pull von ${ref} fehlgeschlagen." + return 1 + fi + + local checksum + checksum="$(resolve_pulled_checksum "$ref")" + if [ -z "$checksum" ]; then + log "FEHLER: Konnte Commit-Checksum fuer ${ref} nach dem Pull nicht ermitteln." + return 1 + fi + if ! ostree --repo="$REPO_PATH" refs --create="$ref" "$checksum" --force; then + log "FEHLER: Konnte heads/-Ref fuer ${ref} nicht anlegen." + return 1 + fi + + # ref hat die Form "app/ID/ARCH/BRANCH" oder "runtime/ID/ARCH/BRANCH". + # Signiert wird mit dem gemeinsamen Key aus ./gpg - demselben, mit dem der + # DNF-Spiegel seine RPMs signiert. + local ref_id ref_arch ref_branch + ref_id="$(echo "$ref" | cut -d/ -f2)" + ref_arch="$(echo "$ref" | cut -d/ -f3)" + ref_branch="$(echo "$ref" | cut -d/ -f4)" + + # "flatpak build-sign" sucht die ID per Default unter app/ - eine Runtime + # muss ausdruecklich als solche gekennzeichnet werden, sonst scheitert es + # mit "Refspec 'app///' not found" und die Runtime bleibt + # unsigniert (Clients mit GPG-Pruefung koennen sie dann nicht installieren). + local -a sign_args=( + --arch="$ref_arch" + --gpg-sign="$GPG_KEY_ID" + --gpg-homedir="$GNUPGHOME" + ) + [ "${ref%%/*}" = "runtime" ] && sign_args+=(--runtime) + + log "Signiere ${ref} mit dem Spiegel-Key" + if ! flatpak build-sign "$REPO_PATH" "$ref_id" "$ref_branch" "${sign_args[@]}"; then + log "FEHLER: Signieren von ${ref} fehlgeschlagen." + return 1 + fi +} + +# Ergebnis von resolve_and_pull_all(): alle Refs, die der Spiegel fuehren soll +# (freigegebene Apps + alle aufgeloesten Runtimes). +# +# Bewusst eine globale Variable statt einer Ausgabe auf stdout: in der +# Funktion laufen ostree und flatpak, deren eigene Ausgabe sonst ueber die +# Kommandosubstitution mit ins "Ergebnis" geraet - im Log tauchten dann +# saemtliche Fortschrittszeilen als vermeintliche Refs auf. +WANTED_REFS=() + +# Nimmt die explizit freigegebenen App-Refs, pullt sie, liest jeweils die +# Runtime-Abhaengigkeit aus und haengt sie hinten an die Warteschlange an - +# so werden auch mehrstufige Abhaengigkeiten (App -> Runtime -> Basis-Runtime) +# vollstaendig aufgeloest. +# +# Ein einzelner fehlgeschlagener Ref bricht den Lauf nicht ab: die uebrigen +# Apps werden trotzdem aktualisiert, der Ref behaelt seinen letzten guten +# Stand (er bleibt in WANTED_REFS und wird deshalb auch nicht geloescht) und +# der Rueckgabewert markiert den Lauf als fehlgeschlagen. +resolve_and_pull_all() { + local -A seen=() + local queue=() + local ref dep status=0 + + WANTED_REFS=() + + while IFS= read -r ref; do + [ -z "$ref" ] && continue + queue+=("$ref") + done < <(explicit_app_refs) + + while [ "${#queue[@]}" -gt 0 ]; do + ref="${queue[0]}" + queue=("${queue[@]:1}") + + [ -n "${seen[$ref]:-}" ] && continue + seen["$ref"]=1 + + if ! pull_one_ref "$ref"; then + log "${ref} wird uebersprungen, letzter guter Stand bleibt erhalten." + status=1 + fi + + # Die Runtime-Abhaengigkeit auch dann aufloesen, wenn der Ref gerade + # fehlgeschlagen ist: sonst fehlte sie in WANTED_REFS und der Abgleich + # in sync_refs() wuerde eine laengst gespiegelte Runtime loeschen, die + # von der App weiterhin gebraucht wird. + dep="$(get_runtime_dependency "$ref")" + if [ -n "$dep" ] && [ -z "${seen[$dep]:-}" ]; then + log " -> benoetigt zusaetzlich: ${dep}" + queue+=("$dep") + fi + done + + [ "${#seen[@]}" -gt 0 ] && WANTED_REFS=("${!seen[@]}") + return "$status" +} + +# --- 6. Abgleich: pullen was fehlt (inkl. Abhaengigkeiten), loeschen was --- +# --- nicht mehr gewuenscht ist ---------------------------------------------- +sync_refs() { + local wanted current ref status=0 + + log "Loese App-Refs und Abhaengigkeiten auf..." + resolve_and_pull_all || status=1 + + # Schutz gegen eine leere/kaputte flatpaks.yaml (oder ein fehlgeschlagenes + # yq): ohne diese Pruefung waere die Liste der gewuenschten Refs leer und + # der Abgleich unten wuerde JEDEN Ref aus dem Spiegel loeschen. + if [ "${#WANTED_REFS[@]}" -eq 0 ]; then + log "FEHLER: flatpaks.yaml ergibt keinen einzigen App-Ref - es wird nichts" + log "geloescht, der bisherige Stand des Repos bleibt unveraendert." + return 1 + fi + + wanted="$(printf '%s\n' "${WANTED_REFS[@]}")" + current="$(current_refs)" + + log "Verwaltete Refs (Apps + Runtimes):" + printf ' + %s\n' "${WANTED_REFS[@]}" + + # Refs entfernen, die weder explizit freigegeben noch als Abhaengigkeit + # noch benoetigt werden + while IFS= read -r ref; do + [ -z "$ref" ] && continue + if ! grep -qxF "$ref" <<< "$wanted"; then + log "Entferne nicht mehr benoetigten Ref: ${ref}" + if ! ostree --repo="$REPO_PATH" refs --delete "$ref"; then + log "FEHLER: Ref ${ref} konnte nicht geloescht werden." + status=1 + fi + fi + done <<< "$current" + + return "$status" +} + +# --- 7. Summary aktualisieren ----------------------------------------------- +update_summary() { + log "Aktualisiere Repo-Summary und Static Deltas" + if ! flatpak build-update-repo \ + --generate-static-deltas \ + --gpg-sign="$GPG_KEY_ID" \ + --gpg-homedir="$GNUPGHOME" \ + "$REPO_PATH"; then + log "FEHLER: Signierte Summary konnte nicht erstellt werden" + return 1 + fi +} + +# --- 8. Client-Dateien schreiben -------------------------------------------- +# Public Key + fertige .flatpakrepo-Datei, damit Clients den Spiegel mit +# einem Kommando und MIT Signaturpruefung einbinden koennen. Beides liegt im +# Repo-Verzeichnis und ist damit unter https:///flatpak/... abrufbar +# (Flathub macht es mit flathub.gpg genauso). +publish_client_files() { + local keyfile="$REPO_PATH/${MIRROR_REPO_ID}.gpg" + + if ! gpg --batch --yes --export --output "$keyfile" "$GPG_KEY_ID"; then + log "FEHLER: Public Key konnte nicht exportiert werden." + return 1 + fi + + cat > "$REPO_PATH/${MIRROR_REPO_ID}.flatpakrepo" < repodata liegt direkt unter /dnf/. + location ~* ^/dnf/repodata/repomd\.xml(\.asc)?$ { + add_header Cache-Control "no-cache"; + } +} diff --git a/sources-repo-template/README.md b/sources-repo-template/README.md new file mode 100644 index 0000000..54e6968 --- /dev/null +++ b/sources-repo-template/README.md @@ -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. diff --git a/sources-repo-template/flatpaks.yaml b/sources-repo-template/flatpaks.yaml new file mode 100644 index 0000000..c6b1a02 --- /dev/null +++ b/sources-repo-template/flatpaks.yaml @@ -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" diff --git a/sources-repo-template/packages.yaml b/sources-repo-template/packages.yaml new file mode 100644 index 0000000..e759e24 --- /dev/null +++ b/sources-repo-template/packages.yaml @@ -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 \ No newline at end of file