Neuer Chart: garage (S3-kompatibler Objektspeicher) 1.0.0+upv2.4.1 #81

Closed
opened 2026-10-03 20:34:10 +02:00 by jlab-bot · 4 comments
Member

Ein Chart für Garage (Deuxfleurs/garage), ein S3-kompatibler Objektspeicher in Rust, gebaut für Selbsthosting über ungleiche Knoten.

Abnehmer ist Stalwarts Blob-Ablage in jlab-cluster/cluster-config — also die Nachrichtenkörper des Mailservers. Der Plan dort: In-Cluster-S3 auf longhorn-encrypted-ns, damit der Verschlüsselungsschlüssel im eigenen Tresor liegt, und Longhorn sichert das verschlüsselte Volume nach OVH. Nachgeprüft: Longhorn verschlüsselt auf Blockebene mit LUKS2 und die Sicherungsmaschine liest unter der Entschlüsselungsschicht, der Sicherungsspeicher sieht also nur Chiffrat.

Dies ist noch kein freigegebener Plan. Anders als in #60 entfällt der Plan-Kommentar aus dem verbindlichen Arbeitsablauf hier nicht — mehrere Entscheidungen unten sind offen und gehören vor der Umsetzung beantwortet.

Der offizielle Chart ist Referenz, nicht Vorlage

Garage liefert einen eigenen Helm-Chart mit, aber nur im Quellbaum unter script/helm, installiert über einen lokalen Pfad:

git clone https://git.deuxfleurs.fr/Deuxfleurs/garage
cd garage/script/helm
helm install --create-namespace --namespace garage garage ./garage

Kein Index, kein OCI-Verweis, kein .tgz — nachgesehen in der offiziellen Kubernetes-Dokumentation. Genau deshalb braucht Fleet einen eigenen Chart: Es zieht über repo: plus version: aus einem Index oder über oci://, nicht aus einem Git-Arbeitsbaum.

Dieser Chart wird nicht kopiert. Er ist die beste verfügbare Quelle dafür, was die Anwendung braucht — welche Ports, welche Volumes, die Discovery-CRD, das RPC-Geheimnis, die Reihenfolge beim ersten Start. Er ist keine Vorlage für die Form. Gebaut wird aus template-chart/ nach CLAUDE.md („Creating a new chart") und dem Stilleitfaden in README.md („Chart style guidelines").

Wo Upstream und unsere Vorgaben auseinandergehen

Das ist die eigentliche Arbeit, und es ist der Grund, warum Kopieren mehr kostet als Neubauen:

Vorgabe hier Was Upstream stattdessen macht
Objektnamen {{ .Release.Name }}-{{ .Chart.Name }}-<kind>, Dateien garage.<kind>.yaml, StatefulSet als garage.sfs.yaml, Services mit -service-Suffix eigene Namensgebung
Geheimnisse als OnePasswordItem über secrets.<name>SecretRef mit op://-Pfad garage.rpc.secret.create legt ein gewöhnliches Secret an
Requests als Werte-Vorgabe, Limits berechnet über den limitFactor-Helfer (Vorgabe 3), keine CPU-Limits ohne ausdrückliche Angabe einfacher resources-Block
Liveness, Readiness und Startup auf jeder Arbeitslast, werte-konfigurierbar mit kalibrierten Vorgaben nicht durchgängig
replicas je Arbeitslast plus chart-globales scaleDown deployment.kind + replicaCount
checksum/config an jeder Pod-Vorlage, die eine ConfigMap liest (#82) — sonst startet eine geänderte Konfiguration nichts neu nicht vorhanden
Härtung: automountServiceAccountToken: false, non-root, readOnlyRootFilesystem: true, Capabilities fallen, schreibbare Pfade als emptyDir; jede durch das Abbild erzwungene Abweichung im values.yaml kommentiert teils vorhanden, nicht nach diesem Muster
values.schema.json mit additionalProperties: false plus die null-sicheren Fallback-Helfer, validiert gegen die echten Bundle-Werte, nicht nur gegen die Vorgaben kein Schema
Versionierung version: <chart-semver>+up<appVersion>, im selben Commit angehoben — jeder Push auf main, der das Verzeichnis berührt, veröffentlicht —

Falls eine Weboberfläche veröffentlicht werden soll, zusätzlich die Ingress-Konvention: ingress.domain und ingress.clusterIssuer required ohne Vorgabe, ingress.className mit Vorgabe nginx, TLS-Secret tls-<release>-<chart>-ingress.

Verifizierte Fakten

Gegen Docker Hub und die Upstream-Dokumentation geprüft, nicht aus der Erinnerung:

Abbild dxflrs/garage (340 Tags)
Neueste Version v2.4.1, 08.09.2026 — davor v2.4.0, v2.3.0, v2.2.0, v2.1.0, v2.0.0
Größe 26,97 MB komprimiert — angenehm klein
Lizenz Apache 2.0, keine bezahlten Stufen, keine Funktionsschranken
Chart-Version daraus 1.0.0+upv2.4.1 (führendes v im appVersion, wie bei stalwart)

Eine Falle für Renovate: Neben den Versionstags schiebt Garage laufend Commit-SHA-Tags (die neuesten vom 03.10.2026). Der customManagers-Block in renovate.json5 braucht deshalb eine Versionsbeschränkung, sonst bietet Renovate 40-stellige Hex-Tags als „Update" an.

Was der Upstream-Chart über die Anwendung verrät

Schlüssel dort Bedeutung
deployment.kind Vorgabe StatefulSet, auf DaemonSet umstellbar
garage.replicationFactor Vorgabe "3"
garage.rpc.bindAddr [::]:3901
garage.rpc.secret.create erzeugt das RPC-Geheimnis beim Ausrollen
garage.metadataAutoSnapshotInterval periodische Schnappschüsse der Metadaten-Datenbank
garage.kubernetesSkipCrd lässt die CRD garagenodes.deuxfleurs.fr für kubernetes_discovery außen vor

Dazu zwei dokumentierte Eigenheiten: Der Chart legt zwei PVCs aus StatefulSet-Vorlagen an (Daten und Metadaten), weshalb das Vergrößern umständlich ist; und nach der Installation muss ein Cluster-Layout angewandt werden, was sich per Job automatisieren lässt.

Offene Entscheidungen für den Plan-Kommentar

  1. Ein Knoten mit replicationFactor: 1 oder ein Cluster? Für den bekannten Abnehmer spricht alles für einen Knoten: longhorn-encrypted-ns repliziert schon dreifach und verschlüsselt. Garage-Replikation 3 über Longhorn-Replikation 3 wäre neunfache Rohdatenmenge, und zwei Schichten, die nichts voneinander wissen.
  2. Ein Volume oder zwei? Longhorn-Schnappschüsse sind je Volume atomar, nicht volumenübergreifend. Mit getrennten Daten- und Metadaten-Volumes kann eine Sicherung Metadaten erfassen, die auf Objekte zeigen, deren Blöcke noch nicht geschrieben waren — bei einem Mailspeicher genau die Fehlerart „Postfach listet eine Nachricht, deren Inhalt fehlt". Zu klären: Lässt Garage beides in einem Verzeichnis?
  3. Die kubernetes_discovery-CRD. Cluster-weit, zwingt im abnehmenden Bundle also defaultNamespace: statt namespace:. Bei einem Knoten ist Discovery vermutlich entbehrlich — dann gehört sie gar nicht in den Chart.
  4. Erststart: Cluster-Layout, Bucket und Zugangsschlüssel. Als Job im Chart, oder Handarbeit mit NOTES.txt als Prüfliste?
  5. LMDB als Metadaten-Engine auf einem netzwerkreplizierten Blockgerät. Die Dokumentation bevorzugt SSD. Messen, nicht annehmen.
  6. Ingress ja oder nein? Die S3-API wird clusterintern von Stalwart gesprochen. Eine Admin-Oberfläche von außen ist eine eigene Entscheidung.

CI-Verdrahtung, nach CLAUDE.md

  1. .github/workflows/publish-garage.yml — eine bestehende Hülle kopieren (z. B. publish-outline.yml), damit uses:, registry-owner und secrets: inherit stimmen. Aus dem Kopf geschrieben stimmt es nicht.
  2. garage in die chart-name-Auswahlliste von .github/workflows/change-app-version.yml eintragen.
  3. Install-Test-Fixtures unter ci/garage/ (test-values.yaml, fixtures.yaml) — oder ein begründeter Eintrag in ci/excluded-charts.txt. Fehlt beides, scheitert der Install-Test im PR.
  4. customManagers-Block in renovate.json5 für das Abbild, mit der Versionsbeschränkung aus der Renovate-Falle oben.

Was nicht in diesen Chart gehört

Die Sicherungs-Label am Volume (recurring-job.longhorn.io/daily-backup und …/daily-snapshot) sind Sache des abnehmenden Bundles in cluster-config, nicht des Charts. Sie greifen nicht automatisch — Stand 02.10.2026 tragen 41 von 62 Volumes im Cluster keine. Hier steht es nur, damit es beim Ausrollen nicht untergeht: Ohne diese Label existiert die OVH-Hälfte des Plans nicht, sie sieht nur so aus.

Ebenso gehört das volume-encryption-Bundle für den Namensraum nach cluster-config — longhorn-encrypted-ns löst ${pvc.namespace}-pvc-encryption-secret auf, und ohne diesen Eintrag bleibt die PVC Pending.

Ein Chart für **Garage** (`Deuxfleurs/garage`), ein S3-kompatibler Objektspeicher in Rust, gebaut für Selbsthosting über ungleiche Knoten. **Abnehmer ist Stalwarts Blob-Ablage** in `jlab-cluster/cluster-config` — also die Nachrichtenkörper des Mailservers. Der Plan dort: In-Cluster-S3 auf `longhorn-encrypted-ns`, damit der Verschlüsselungsschlüssel im eigenen Tresor liegt, und Longhorn sichert das verschlüsselte Volume nach OVH. Nachgeprüft: Longhorn verschlüsselt auf Blockebene mit LUKS2 und die Sicherungsmaschine liest *unter* der Entschlüsselungsschicht, der Sicherungsspeicher sieht also nur Chiffrat. > **Dies ist noch kein freigegebener Plan.** Anders als in #60 entfällt der Plan-Kommentar aus dem verbindlichen Arbeitsablauf hier **nicht** — mehrere Entscheidungen unten sind offen und gehören vor der Umsetzung beantwortet. ## Der offizielle Chart ist Referenz, nicht Vorlage Garage liefert einen eigenen Helm-Chart mit, aber **nur im Quellbaum** unter `script/helm`, installiert über einen lokalen Pfad: ``` git clone https://git.deuxfleurs.fr/Deuxfleurs/garage cd garage/script/helm helm install --create-namespace --namespace garage garage ./garage ``` Kein Index, kein OCI-Verweis, kein `.tgz` — nachgesehen in der offiziellen Kubernetes-Dokumentation. Genau deshalb braucht Fleet einen eigenen Chart: Es zieht über `repo:` plus `version:` aus einem Index oder über `oci://`, nicht aus einem Git-Arbeitsbaum. **Dieser Chart wird nicht kopiert.** Er ist die beste verfügbare Quelle dafür, *was die Anwendung braucht* — welche Ports, welche Volumes, die Discovery-CRD, das RPC-Geheimnis, die Reihenfolge beim ersten Start. Er ist **keine** Vorlage für die Form. Gebaut wird aus `template-chart/` nach `CLAUDE.md` („Creating a new chart") und dem Stilleitfaden in `README.md` („Chart style guidelines"). ### Wo Upstream und unsere Vorgaben auseinandergehen Das ist die eigentliche Arbeit, und es ist der Grund, warum Kopieren mehr kostet als Neubauen: | Vorgabe hier | Was Upstream stattdessen macht | |---|---| | Objektnamen `{{ .Release.Name }}-{{ .Chart.Name }}-<kind>`, Dateien `garage.<kind>.yaml`, StatefulSet als `garage.sfs.yaml`, Services mit **`-service`**-Suffix | eigene Namensgebung | | Geheimnisse als `OnePasswordItem` über `secrets.<name>SecretRef` mit `op://`-Pfad | `garage.rpc.secret.create` legt ein gewöhnliches Secret an | | Requests als Werte-Vorgabe, **Limits berechnet** über den `limitFactor`-Helfer (Vorgabe 3), keine CPU-Limits ohne ausdrückliche Angabe | einfacher `resources`-Block | | Liveness, Readiness **und** Startup auf jeder Arbeitslast, werte-konfigurierbar mit kalibrierten Vorgaben | nicht durchgängig | | `replicas` je Arbeitslast plus chart-globales `scaleDown` | `deployment.kind` + `replicaCount` | | `checksum/config` an jeder Pod-Vorlage, die eine ConfigMap liest (#82) — sonst startet eine geänderte Konfiguration nichts neu | nicht vorhanden | | Härtung: `automountServiceAccountToken: false`, non-root, `readOnlyRootFilesystem: true`, Capabilities fallen, schreibbare Pfade als `emptyDir`; jede durch das Abbild erzwungene Abweichung **im `values.yaml` kommentiert** | teils vorhanden, nicht nach diesem Muster | | `values.schema.json` mit `additionalProperties: false` plus die null-sicheren Fallback-Helfer, **validiert gegen die echten Bundle-Werte**, nicht nur gegen die Vorgaben | kein Schema | | Versionierung `version: <chart-semver>+up<appVersion>`, im selben Commit angehoben — jeder Push auf `main`, der das Verzeichnis berührt, veröffentlicht | — | Falls eine Weboberfläche veröffentlicht werden soll, zusätzlich die Ingress-Konvention: `ingress.domain` und `ingress.clusterIssuer` `required` **ohne** Vorgabe, `ingress.className` mit Vorgabe `nginx`, TLS-Secret `tls-<release>-<chart>-ingress`. ## Verifizierte Fakten Gegen Docker Hub und die Upstream-Dokumentation geprüft, nicht aus der Erinnerung: | | | |---|---| | Abbild | `dxflrs/garage` (340 Tags) | | Neueste Version | **`v2.4.1`**, 08.09.2026 — davor `v2.4.0`, `v2.3.0`, `v2.2.0`, `v2.1.0`, `v2.0.0` | | Größe | **26,97 MB komprimiert** — angenehm klein | | Lizenz | Apache 2.0, keine bezahlten Stufen, keine Funktionsschranken | | Chart-Version daraus | `1.0.0+upv2.4.1` (führendes `v` im `appVersion`, wie bei `stalwart`) | **Eine Falle für Renovate:** Neben den Versionstags schiebt Garage **laufend Commit-SHA-Tags** (die neuesten vom 03.10.2026). Der `customManagers`-Block in `renovate.json5` braucht deshalb eine Versionsbeschränkung, sonst bietet Renovate 40-stellige Hex-Tags als „Update" an. ### Was der Upstream-Chart über die Anwendung verrät | Schlüssel dort | Bedeutung | |---|---| | `deployment.kind` | Vorgabe `StatefulSet`, auf `DaemonSet` umstellbar | | `garage.replicationFactor` | Vorgabe **`"3"`** | | `garage.rpc.bindAddr` | `[::]:3901` | | `garage.rpc.secret.create` | erzeugt das RPC-Geheimnis beim Ausrollen | | `garage.metadataAutoSnapshotInterval` | periodische Schnappschüsse der Metadaten-Datenbank | | `garage.kubernetesSkipCrd` | lässt die CRD `garagenodes.deuxfleurs.fr` für `kubernetes_discovery` außen vor | Dazu zwei dokumentierte Eigenheiten: Der Chart legt **zwei** PVCs aus StatefulSet-Vorlagen an (Daten und Metadaten), weshalb das Vergrößern umständlich ist; und nach der Installation muss ein Cluster-Layout angewandt werden, was sich per Job automatisieren lässt. ## Offene Entscheidungen für den Plan-Kommentar 1. **Ein Knoten mit `replicationFactor: 1` oder ein Cluster?** Für den bekannten Abnehmer spricht alles für einen Knoten: `longhorn-encrypted-ns` repliziert schon dreifach und verschlüsselt. Garage-Replikation 3 **über** Longhorn-Replikation 3 wäre neunfache Rohdatenmenge, und zwei Schichten, die nichts voneinander wissen. 2. **Ein Volume oder zwei?** Longhorn-Schnappschüsse sind **je Volume** atomar, nicht volumenübergreifend. Mit getrennten Daten- und Metadaten-Volumes kann eine Sicherung Metadaten erfassen, die auf Objekte zeigen, deren Blöcke noch nicht geschrieben waren — bei einem Mailspeicher genau die Fehlerart „Postfach listet eine Nachricht, deren Inhalt fehlt". Zu klären: Lässt Garage beides in einem Verzeichnis? 3. **Die `kubernetes_discovery`-CRD.** Cluster-weit, zwingt im abnehmenden Bundle also `defaultNamespace:` statt `namespace:`. Bei einem Knoten ist Discovery vermutlich entbehrlich — dann gehört sie gar nicht in den Chart. 4. **Erststart:** Cluster-Layout, Bucket und Zugangsschlüssel. Als Job im Chart, oder Handarbeit mit `NOTES.txt` als Prüfliste? 5. **LMDB als Metadaten-Engine auf einem netzwerkreplizierten Blockgerät.** Die Dokumentation bevorzugt SSD. Messen, nicht annehmen. 6. **Ingress ja oder nein?** Die S3-API wird clusterintern von Stalwart gesprochen. Eine Admin-Oberfläche von außen ist eine eigene Entscheidung. ## CI-Verdrahtung, nach `CLAUDE.md` 1. `.github/workflows/publish-garage.yml` — eine bestehende Hülle kopieren (z. B. `publish-outline.yml`), damit `uses:`, `registry-owner` und `secrets: inherit` stimmen. Aus dem Kopf geschrieben stimmt es nicht. 2. `garage` in die `chart-name`-Auswahlliste von `.github/workflows/change-app-version.yml` eintragen. 3. Install-Test-Fixtures unter `ci/garage/` (`test-values.yaml`, `fixtures.yaml`) — **oder** ein begründeter Eintrag in `ci/excluded-charts.txt`. Fehlt beides, scheitert der Install-Test im PR. 4. `customManagers`-Block in `renovate.json5` für das Abbild, mit der Versionsbeschränkung aus der Renovate-Falle oben. ## Was nicht in diesen Chart gehört Die **Sicherungs-Label** am Volume (`recurring-job.longhorn.io/daily-backup` und `…/daily-snapshot`) sind Sache des abnehmenden Bundles in `cluster-config`, nicht des Charts. Sie greifen **nicht** automatisch — Stand 02.10.2026 tragen 41 von 62 Volumes im Cluster keine. Hier steht es nur, damit es beim Ausrollen nicht untergeht: Ohne diese Label existiert die OVH-Hälfte des Plans nicht, sie sieht nur so aus. Ebenso gehört das `volume-encryption`-Bundle für den Namensraum nach `cluster-config` — `longhorn-encrypted-ns` löst `${pvc.namespace}-pvc-encryption-secret` auf, und ohne diesen Eintrag bleibt die PVC `Pending`.
jlab-bot added the inhouse-chart label 2026-10-03 20:34:10 +02:00
Author
Member

Freigegebener Umsetzungsplan

Die sechs offenen Entscheidungen sind beantwortet: zwei vom Betreuer, vier aus der Dokumentation. Dabei hat sich die Sicherungsprämisse des Issue-Bodys an einer tragenden Stelle als unvollständig erwiesen — das steht unten zuerst, weil der Rest darauf aufbaut.

Die Korrektur: Longhorn sichert die Metadaten nicht

Der Issue-Body baut darauf, dass Longhorn das verschlüsselte Volume nach OVH sichert. Für die Datenblöcke stimmt das. Für die Metadaten nicht: Dateisystem-Schnappschüsse von metadata_dir garantieren laut Garage keine Konsistenz — wird während eines Metadaten-Schreibvorgangs geschnappschosst, ist der Schnappschuss selbst beschädigt und als Rücksetzpunkt unbrauchbar. Garage empfiehlt deshalb in allen Fällen seinen eigenen metadata_auto_snapshot_interval, der die Datenbank im Betrieb konsistent kopiert (Known issues, Durability & Repairs).

Einzuplanen: Garage behält zwei Schnappschüsse, der erste etwa drei Stunden nach dem Start. Bei "6h" ist das Rücksetzfenster rund 6–12 Stunden — wird eine Beschädigung später entdeckt, ist sie älter als jeder Schnappschuss.

Und ein Standardwert, der die Härtung aushebelt: metadata_fsync ist standardmäßig aus. Longhorns Blockschicht hilft nicht, wenn die Engine nie fsync aufruft. Garages eigene Beispielkonfiguration setzt metadata_fsync = true neben metadata_auto_snapshot_interval = "6h". Bei uns wird es gesetzt, nicht geerbt.

Entscheidung 1: ein Knoten, replication_factor = 1

Die Begründung des Issues trägt für die Datenblöcke: longhorn-encrypted-ns repliziert dreifach und verschlüsselt, Garage-RF-3 darüber wäre neunfache Rohdatenmenge und zwei Schichten, die nichts voneinander wissen.

Was dabei bewusst in Kauf genommen wird, und es gehört benannt: Upstream schreibt zu replication_factor = 1 ausdrücklich „Do not use this for anything else than test deployments", und die Known-Issues-Seite nennt den Grund — derselbe replication_factor gilt auch für die Metadaten, RF 1 ist also besonders anfällig für Metadaten-Beschädigung, und eine beschädigte Datenbankdatei ist „generally not recoverable". Longhorn hilft dagegen nicht: es repliziert eine beschädigte Datei dreifach. Redundanz gegen Geräteausfall ja, gegen Dateibeschädigung nein.

Das ist eine abgewogene Annahme des Risikos, keine Übersehung. Die Gegenmaßnahmen, die bei RF 1 überhaupt greifen, sind alle Pflicht und nicht optional:

  • metadata_fsync = true
  • metadata_auto_snapshot_interval gesetzt
  • die Schnappschüsse über metadata_snapshots_dir auf einem anderen Volume als metadata_dir

Der Chart führt replicationFactor als Wert, damit eine späterer Erhöhung nicht am Chart scheitert. Nicht als Ausbauweg dokumentieren, solange nicht belegt ist, wie eine Erhöhung in Garage abläuft — eine Faktor-Erhöhung ist ein Layout-Vorgang mit Datenbewegung, kein Schalter.

Entscheidung 2: db_engine = "lmdb", abgesichert

Garages Vorgabe seit 0.9 und die schnellere Engine. Der Preis ist benannt: die gemeldete Beschädigung nach unsauberem Herunterfahren bleibt möglich, die Absicherung senkt nur die Wahrscheinlichkeit, und bei RF 1 gibt es keinen Mitknoten zur Wiederherstellung — nur den letzten Schnappschuss. Die Empfehlung für LMDB ist in der Doku an RF ≥ 2 gebunden; dass wir sie bei RF 1 fahren, gehört als Kommentar an den Wert, zusammen mit sqlite als der Alternative und dem Grund, warum sie nicht gewählt wurde (Metadaten-Durchsatz, bei einem Mailserver als einzigem Abnehmer kaum die Grenze).

Entscheidung 3: zwei Volumes, anders aufgeteilt als gedacht

Nicht „Daten und Metadaten getrennt", sondern:

PVC Inhalt Begründung
1 metadata_dir und data_dir (zwei Unterverzeichnisse) Longhorn-Schnappschüsse sind je Volume atomar, also bleiben Index und Blöcke zueinander konsistent — genau der Fehler „Postfach listet eine Nachricht, deren Inhalt fehlt" wird damit vermieden
2 metadata_snapshots_dir Der konsistente Rücksetzpunkt darf nicht mit dem Volume sterben, dessen Beschädigung er heilen soll

Der exakte Schlüsselname metadata_snapshots_dir ist gegen die Konfigurationsreferenz zu bestätigen, nicht zu übernehmen.

Entscheidung 4: keine Discovery-CRD

Ein einzelner Knoten hat keine Mitglieder zu finden. Die CRD garagenodes.deuxfleurs.fr ist cluster-weit und würde das abnehmende Bundle auf defaultNamespace: statt namespace: zwingen — für nichts. Bleibt draußen; kubernetes_discovery wird nicht konfiguriert.

Entscheidung 5: Erststart von Hand, NOTES.txt als Prüfliste

Nicht Bequemlichkeit, sondern zweifach erzwungen:

  1. garage layout apply --version <N+1> verlangt genau die Vorgängerversion plus eins, und „never make multiple calls … with the same value of the --version flag". Ein Job, der bei helm upgrade erneut anläuft, ist also gefährlich, nicht bloß unnötig.
  2. Der Zugangsschlüssel, den Garage erzeugt, muss in das 1Password-Item wandern, damit Stalwart ihn liest. Das kann kein Job.

NOTES.txt trägt die Schritte in der richtigen Reihenfolge mit den echten Befehlen: garage status, garage layout assign …, garage layout show, garage layout apply --version <N+1>, Bucket anlegen, Zugangsschlüssel erzeugen, Schlüssel an den Bucket binden, und zum Schluss die Felder, die ins Tresor-Item gehören.

Entscheidung 6: kein Ingress

Die S3-API (3900) spricht Stalwart clusterintern, die Admin-API (3903) ebenso. Der Wert kommt als ingress.enabled: false mit, damit er existiert, wenn die Oberfläche doch veröffentlicht werden soll — dann nach unserer Ingress-Konvention (domain und clusterIssuer required ohne Vorgabe, className mit Vorgabe nginx, TLS-Secret tls-<release>-garage-ingress).

Verifizierte Eckdaten

Ports S3 [::]:3900, RPC [::]:3901, Web [::]:3902, Admin 0.0.0.0:3903
Engines lmdb (Vorgabe seit 0.9.0), sqlite, fjall (experimentell)
RPC-Geheimnis 32 Byte hex; rpc_secret_file existiert — kommt als Datei aus einem OnePasswordItem, nicht in die ConfigMap
Version 1.0.0+upv2.4.1, appVersion v2.4.1

Umfang

Gebaut aus template-chart/ nach CLAUDE.md und dem Stilleitfaden, nicht aus Upstreams Chart kopiert — der ist Quelle für was die Anwendung braucht, nicht für die Form. StatefulSet (garage.sfs.yaml), ConfigMap mit checksum/config, zwei PVCs, Service, OnePasswordItem für das RPC-Geheimnis, Probes auf allen drei Arten, berechnete Limits über den limitFactor-Helfer, scaleDown, strenges values.schema.json mit den null-sicheren Fallback-Helfern, validiert gegen die echten Bundle-Werte.

CI: publish-garage.yml aus einer bestehenden Hülle kopiert; garage in die chart-name-Liste von change-app-version.yml; Fixtures unter ci/garage/ oder ein begründeter Ausschluss; customManagers-Block in renovate.json5 mit Versionsbeschränkung, denn Garage schiebt laufend Commit-SHA-Tags und Renovate bietet sonst 40-stellige Hex-Tags als Update an.

Wiki im selben Durchgang: neue Chart-Seite garage mit Werte-Tabelle, der Erststart-Prüfliste und einem Traps-Abschnitt (die Metadaten-Sicherungslücke, metadata_fsync aus Vorgabe, das Zwei-Schnappschuss-Fenster, die Nicht-Idempotenz von layout apply, RF 1 und Metadaten); Zeile in Charts; Eintrag in _Sidebar.md; Lessons-Learned für die allgemeine Form „der Schnappschuss, der keinen Rücksetzpunkt war" — die wiederholt sich bei jeder eingebetteten Datenbank auf einem Volume mit Schnappschüssen.

Offen, am laufenden Dienst zu messen

  • Arbeitet Garage mit metadata_dir und data_dir als Unterverzeichnisse einer PVC ohne Beanstandung?
  • Greift metadata_auto_snapshot_interval wirklich und landet in metadata_snapshots_dir?
  • Startet die S3-API vor dem Anwenden des Layouts, oder antwortet sie bis dahin mit Fehler? Davon hängt ab, worauf die Readiness-Probe zeigen darf — eine Probe, die vor dem Layout grün wird, verspricht einen Dienst, der nichts annimmt.
  • Hält readOnlyRootFilesystem: true mit den zwei PVCs und welche weiteren Schreibpfade als emptyDir nötig sind.

Nicht in diesem Chart

Die Sicherungs-Label am Volume (recurring-job.longhorn.io/daily-backup, …/daily-snapshot) und das volume-encryption-Bundle für den Namensraum gehören nach cluster-config. Ohne die Label existiert die OVH-Hälfte des Plans nicht, sie sieht nur so aus; ohne den Verschlüsselungs-Eintrag bleibt die PVC Pending.

# Freigegebener Umsetzungsplan Die sechs offenen Entscheidungen sind beantwortet: zwei vom Betreuer, vier aus der Dokumentation. Dabei hat sich die **Sicherungsprämisse des Issue-Bodys an einer tragenden Stelle als unvollständig erwiesen** — das steht unten zuerst, weil der Rest darauf aufbaut. ## Die Korrektur: Longhorn sichert die Metadaten nicht Der Issue-Body baut darauf, dass Longhorn das verschlüsselte Volume nach OVH sichert. Für die **Datenblöcke** stimmt das. Für die **Metadaten nicht**: Dateisystem-Schnappschüsse von `metadata_dir` garantieren laut Garage keine Konsistenz — wird während eines Metadaten-Schreibvorgangs geschnappschosst, ist der Schnappschuss selbst beschädigt und als Rücksetzpunkt unbrauchbar. Garage empfiehlt deshalb **in allen Fällen** seinen eigenen `metadata_auto_snapshot_interval`, der die Datenbank im Betrieb konsistent kopiert ([Known issues](https://garagehq.deuxfleurs.fr/documentation/reference-manual/known-issues/), [Durability & Repairs](https://garagehq.deuxfleurs.fr/documentation/operations/durability-repairs/)). Einzuplanen: Garage behält **zwei** Schnappschüsse, der erste etwa drei Stunden nach dem Start. Bei `"6h"` ist das Rücksetzfenster rund 6–12 Stunden — wird eine Beschädigung später entdeckt, ist sie älter als jeder Schnappschuss. **Und ein Standardwert, der die Härtung aushebelt: `metadata_fsync` ist standardmäßig aus.** Longhorns Blockschicht hilft nicht, wenn die Engine nie `fsync` aufruft. Garages eigene Beispielkonfiguration setzt `metadata_fsync = true` neben `metadata_auto_snapshot_interval = "6h"`. Bei uns wird es **gesetzt, nicht geerbt**. ## Entscheidung 1: ein Knoten, `replication_factor = 1` Die Begründung des Issues trägt für die **Datenblöcke**: longhorn-encrypted-ns repliziert dreifach und verschlüsselt, Garage-RF-3 darüber wäre neunfache Rohdatenmenge und zwei Schichten, die nichts voneinander wissen. **Was dabei bewusst in Kauf genommen wird, und es gehört benannt:** Upstream schreibt zu `replication_factor = 1` ausdrücklich „Do not use this for anything else than test deployments", und die Known-Issues-Seite nennt den Grund — **derselbe `replication_factor` gilt auch für die Metadaten**, RF 1 ist also besonders anfällig für Metadaten-Beschädigung, und eine beschädigte Datenbankdatei ist „generally not recoverable". Longhorn hilft dagegen **nicht**: es repliziert eine beschädigte Datei dreifach. Redundanz gegen Geräteausfall ja, gegen Dateibeschädigung nein. Das ist eine abgewogene Annahme des Risikos, keine Übersehung. Die Gegenmaßnahmen, die bei RF 1 überhaupt greifen, sind alle Pflicht und nicht optional: - `metadata_fsync = true` - `metadata_auto_snapshot_interval` gesetzt - die Schnappschüsse über `metadata_snapshots_dir` auf einem **anderen Volume** als `metadata_dir` Der Chart führt `replicationFactor` als Wert, damit eine späterer Erhöhung nicht am Chart scheitert. **Nicht** als Ausbauweg dokumentieren, solange nicht belegt ist, wie eine Erhöhung in Garage abläuft — eine Faktor-Erhöhung ist ein Layout-Vorgang mit Datenbewegung, kein Schalter. ## Entscheidung 2: `db_engine = "lmdb"`, abgesichert Garages Vorgabe seit 0.9 und die schnellere Engine. Der Preis ist benannt: die gemeldete Beschädigung nach unsauberem Herunterfahren bleibt möglich, die Absicherung senkt nur die Wahrscheinlichkeit, und bei RF 1 gibt es keinen Mitknoten zur Wiederherstellung — nur den letzten Schnappschuss. Die Empfehlung für LMDB ist in der Doku an RF ≥ 2 gebunden; dass wir sie bei RF 1 fahren, gehört als Kommentar an den Wert, zusammen mit `sqlite` als der Alternative und dem Grund, warum sie nicht gewählt wurde (Metadaten-Durchsatz, bei einem Mailserver als einzigem Abnehmer kaum die Grenze). ## Entscheidung 3: zwei Volumes, anders aufgeteilt als gedacht Nicht „Daten und Metadaten getrennt", sondern: | PVC | Inhalt | Begründung | | --- | --- | --- | | 1 | `metadata_dir` **und** `data_dir` (zwei Unterverzeichnisse) | Longhorn-Schnappschüsse sind je Volume atomar, also bleiben Index und Blöcke zueinander konsistent — genau der Fehler „Postfach listet eine Nachricht, deren Inhalt fehlt" wird damit vermieden | | 2 | `metadata_snapshots_dir` | Der konsistente Rücksetzpunkt darf nicht mit dem Volume sterben, dessen Beschädigung er heilen soll | Der exakte Schlüsselname `metadata_snapshots_dir` ist gegen die Konfigurationsreferenz zu bestätigen, nicht zu übernehmen. ## Entscheidung 4: keine Discovery-CRD Ein einzelner Knoten hat keine Mitglieder zu finden. Die CRD `garagenodes.deuxfleurs.fr` ist cluster-weit und würde das abnehmende Bundle auf `defaultNamespace:` statt `namespace:` zwingen — für nichts. Bleibt draußen; `kubernetes_discovery` wird nicht konfiguriert. ## Entscheidung 5: Erststart von Hand, `NOTES.txt` als Prüfliste Nicht Bequemlichkeit, sondern zweifach erzwungen: 1. `garage layout apply --version <N+1>` verlangt **genau** die Vorgängerversion plus eins, und „never make multiple calls … with the same value of the `--version` flag". Ein Job, der bei `helm upgrade` erneut anläuft, ist also gefährlich, nicht bloß unnötig. 2. Der Zugangsschlüssel, den Garage erzeugt, muss in das 1Password-Item wandern, damit Stalwart ihn liest. Das kann kein Job. `NOTES.txt` trägt die Schritte in der richtigen Reihenfolge mit den echten Befehlen: `garage status`, `garage layout assign …`, `garage layout show`, `garage layout apply --version <N+1>`, Bucket anlegen, Zugangsschlüssel erzeugen, Schlüssel an den Bucket binden, und zum Schluss die Felder, die ins Tresor-Item gehören. ## Entscheidung 6: kein Ingress Die S3-API (3900) spricht Stalwart clusterintern, die Admin-API (3903) ebenso. Der Wert kommt als `ingress.enabled: false` mit, damit er existiert, wenn die Oberfläche doch veröffentlicht werden soll — dann nach unserer Ingress-Konvention (`domain` und `clusterIssuer` required ohne Vorgabe, `className` mit Vorgabe `nginx`, TLS-Secret `tls-<release>-garage-ingress`). ## Verifizierte Eckdaten | | | | --- | --- | | Ports | S3 `[::]:3900`, RPC `[::]:3901`, Web `[::]:3902`, Admin `0.0.0.0:3903` | | Engines | `lmdb` (Vorgabe seit 0.9.0), `sqlite`, `fjall` (experimentell) | | RPC-Geheimnis | 32 Byte hex; **`rpc_secret_file` existiert** — kommt als Datei aus einem `OnePasswordItem`, nicht in die ConfigMap | | Version | `1.0.0+upv2.4.1`, appVersion `v2.4.1` | ## Umfang Gebaut aus `template-chart/` nach CLAUDE.md und dem Stilleitfaden, **nicht** aus Upstreams Chart kopiert — der ist Quelle für *was die Anwendung braucht*, nicht für die Form. StatefulSet (`garage.sfs.yaml`), ConfigMap mit `checksum/config`, zwei PVCs, Service, `OnePasswordItem` für das RPC-Geheimnis, Probes auf allen drei Arten, berechnete Limits über den `limitFactor`-Helfer, `scaleDown`, strenges `values.schema.json` mit den null-sicheren Fallback-Helfern, validiert **gegen die echten Bundle-Werte**. CI: `publish-garage.yml` aus einer bestehenden Hülle **kopiert**; `garage` in die `chart-name`-Liste von `change-app-version.yml`; Fixtures unter `ci/garage/` oder ein begründeter Ausschluss; `customManagers`-Block in `renovate.json5` **mit Versionsbeschränkung**, denn Garage schiebt laufend Commit-SHA-Tags und Renovate bietet sonst 40-stellige Hex-Tags als Update an. Wiki im selben Durchgang: neue Chart-Seite `garage` mit Werte-Tabelle, der Erststart-Prüfliste und einem Traps-Abschnitt (die Metadaten-Sicherungslücke, `metadata_fsync` aus Vorgabe, das Zwei-Schnappschuss-Fenster, die Nicht-Idempotenz von `layout apply`, RF 1 und Metadaten); Zeile in `Charts`; Eintrag in `_Sidebar.md`; `Lessons-Learned` für die allgemeine Form „der Schnappschuss, der keinen Rücksetzpunkt war" — die wiederholt sich bei jeder eingebetteten Datenbank auf einem Volume mit Schnappschüssen. ## Offen, am laufenden Dienst zu messen - Arbeitet Garage mit `metadata_dir` und `data_dir` als Unterverzeichnisse **einer** PVC ohne Beanstandung? - Greift `metadata_auto_snapshot_interval` wirklich und landet in `metadata_snapshots_dir`? - Startet die S3-API vor dem Anwenden des Layouts, oder antwortet sie bis dahin mit Fehler? Davon hängt ab, worauf die Readiness-Probe zeigen darf — eine Probe, die vor dem Layout grün wird, verspricht einen Dienst, der nichts annimmt. - Hält `readOnlyRootFilesystem: true` mit den zwei PVCs und welche weiteren Schreibpfade als emptyDir nötig sind. ## Nicht in diesem Chart Die Sicherungs-Label am Volume (`recurring-job.longhorn.io/daily-backup`, `…/daily-snapshot`) und das `volume-encryption`-Bundle für den Namensraum gehören nach `cluster-config`. Ohne die Label existiert die OVH-Hälfte des Plans nicht, sie sieht nur so aus; ohne den Verschlüsselungs-Eintrag bleibt die PVC `Pending`.
Author
Member

Anhalten vor der Umsetzung: zwei Funde am freigegebenen Plan

Gegen den Quellbaum von v2.4.1 geprüft (codeload.github.com/deuxfleurs-org/garage am Tag v2.4.1, dem Spiegel des Deuxfleurs-Gitea — das Original liegt hinter Anubis und ist aus dem Workspace nicht abrufbar). Zwei Punkte des Plans halten der Quelle nicht stand, einer davon so, dass er in der vorgesehenen Form nicht startet. Gebaut ist noch nichts.


Fund 1 — rpc_secret_file aus einem Kubernetes-Secret ist im gehärteten Pod nicht lesbar

Der Plan hält fest: „rpc_secret_file existiert — kommt als Datei aus einem OnePasswordItem, nicht in die ConfigMap". Der Schlüssel existiert (src/util/config.rs:96), aber die Datei kann unser Container nicht lesen. Das ist keine Konfigurationsfrage, es sind zwei Prüfungen, die sich widersprechen:

Garage, src/garage/secrets.rs:135-147:

if metadata.mode() & 0o077 != 0 {
    return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)\n\
        Refusing to start until this is fixed, or environment variable \
        GARAGE_ALLOW_WORLD_READABLE_SECRETS is set to true.", ...).into());
}

Die Maske ist 0o077 — sie verwirft jedes Gruppen- oder Fremdbit, nicht nur die Fremdbits, trotz des Wortes „world-readable" in der Meldung.

Das Kubelet, pkg/volume/volume_linux.go:72-104 (geprüft an release-1.31): sobald fsGroup gesetzt ist, läuft über jede Datei des Volumes

err := os.Lchown(filename, -1, int(*fsGroup))
mask := rwMask            // 0660
if readonly { mask = roMask }  // 0440 — Secret-Volumes melden ReadOnly: true
err = os.Chmod(filename, info.Mode()|mask)

Das Gruppen-Lesebit wird also hinzu-ge-ODERt, unabhängig von defaultMode. Damit ist mode & 0o077 != 0 immer wahr und Garage verweigert den Start.

Und es gibt keine Kombination, die beides erfüllt, weil Kubernetes an einer Secret-Volume-Datei nur die Gruppe setzen kann, niemals den besitzenden Benutzer:

Variante Modus / Eigentümer Ergebnis
fsGroup gesetzt, defaultMode beliebig root:1000, Gruppenbit immer gesetzt Garage verweigert den Start (0o077-Prüfung)
kein fsGroup, defaultMode: 0400 root:root 0400 Prüfung besteht, aber uid 1000 bekommt EACCES — „Failed to read secret file"
kein fsGroup, Vorgabe 0644 root:root 0644 Garage verweigert den Start
runAsUser: 0 — widerspricht der Härtung

Zwei Wege hinaus, und sie sind nicht gleichwertig:

(a) GARAGE_RPC_SECRET als env.valueFrom.secretKeyRef. src/garage/secrets.rs:22-28 führt den Wert mit env = "GARAGE_RPC_SECRET", und src/garage/main.rs:50 flacht Secrets in die globalen Optionen ein — die Variable gilt also für garage server genauso wie für kubectl exec … -- /garage status, das dieselbe Berechtigung braucht. Keine Datei, keine Dateirechte, keine abgeschaltete Prüfung, und das eigentliche Ziel des Plans ist erfüllt: das Geheimnis bleibt ein Secret und landet nicht in der ConfigMap.

(b) rpc_secret_file + allow_world_readable_secrets = true. Funktioniert, kostet aber die Prüfung für alle Geheimnisdateien dieses Prozesses — und die Datei ist dabei gar nicht fremdlesbar, Garage kann es nur nicht unterscheiden. Upstream kennt den Fall übrigens: der Doc-Kommentar am Wert lautet „Skip the permission check of secret files. Useful when POSIX ACLs (or more complex chmods) are used." (src/util/config.rs:86-89) — der Schalter ist für genau diese Lage gedacht, nur eben pauschal.

Mein Vorschlag: (a), mit dem Mechanismus als Kommentar an secrets.rpcSecretRef in values.yaml, plus ein Lessons-Learned-Eintrag zur allgemeinen Form: eine Rechteprüfung, die annimmt, ihr Dateisystem zu besitzen, trifft auf eine Plattform, die nur die Gruppe setzen kann — das wiederholt sich bei jedem Programm mit *_secret_file und einem 0o077-Test (Garage selbst hat drei davon: rpc_secret_file, admin_token_file, metrics_token_file).


Fund 2 — garage server --single-node macht genau das, wofür Entscheidung 5 Handarbeit vorsieht, und entkräftet beide Begründungen

Entscheidung 5 steht auf zwei Prämissen. Beide treffen für v2.4.1 bei einem Knoten nicht mehr zu.

Die Schalter, src/garage/cli/structs.rs:95-108:

pub struct ServerOpt {
    /// Automatically configure a single-node layout in the cluster.
    /// Garage will refuse to run if the cluster already has other nodes.
    #[structopt(long = "single-node")]
    pub(crate) single_node: bool,
    /// Configure a default S3 API key using environment variables
    /// `GARAGE_DEFAULT_ACCESS_KEY` and `GARAGE_DEFAULT_SECRET_KEY`. Requires `--single-node`.
    #[structopt(long = "default-access-key")]
    pub(crate) default_access_key: bool,
    /// Configure a default bucket using environment variable `GARAGE_DEFAULT_BUCKET`.
    /// Implies `--default-access-key`. Requires `--single-node`.
    #[structopt(long = "default-bucket")]
    pub(crate) default_bucket: bool,
}

Prämisse 1 („layout apply --version N+1 ist nicht idempotent, ein Job bei helm upgrade ist gefährlich") fällt. initial_config in src/garage/server.rs:190-256 prüft die Layout-Version im Prozess, nicht über einen wiederholbaren CLI-Aufruf:

  • replication_factor != 1 → Abbruch mit „Single-node mode requires replication_factor = 1 in the configuration file." (genau unsere Konfiguration, also kein Hindernis)
  • Layout-Version 0 → Layout wird angelegt: Zone dc1, Kapazität aus der erkannten Plattengröße, Tag default, dann apply_staged_changes(1)
  • Layout-Version 1 → nichts passiert, der Start läuft normal weiter
  • Layout-Version > 1 → Abbruch mit „Refusing to run in single-node mode: layout version is already superior to 1. Remove the --single-node flag to run the server in full mode."
  • fremde Knoten bekannt → Abbruch mit „Refusing to run in single-node mode: more nodes are already present in the cluster."

Das ist über Neustarts hinweg idempotent, und die Falle, vor der der Plan zu Recht warnt, entsteht gar nicht: es gibt keinen --version-Parameter, den jemand zweimal mit demselben Wert aufrufen könnte.

Prämisse 2 („Der Zugangsschlüssel, den Garage erzeugt, muss in das 1Password-Item wandern. Das kann kein Job.") fällt und kehrt sich um. --default-access-key liest GARAGE_DEFAULT_ACCESS_KEY und GARAGE_DEFAULT_SECRET_KEY aus der Umgebung und importiert den Schlüssel (server.rs:259-300, Key::import in Zeile 291). Die Richtung ist damit die bessere: der Betreuer erzeugt das Schlüsselpaar im Tresor, das Item ist die Quelle der Wahrheit, und Garage übernimmt es — statt dass Garage es erzeugt und es von Hand hinterherkopiert wird. Der Wiederholungsfall ist in der Quelle abgesichert: existierender Schlüssel mit anderem Geheimnis → Fehler; im Cluster gelöschter Schlüssel → Fehler („cannot add it back"). --default-bucket legt zusätzlich den Bucket an und bindet den Schlüssel daran.

Dazu die Reihenfolge, die für die Readiness-Entscheidung zählt: initial_config(&garage, opt).await? steht in server.rs:54 — vor dem Start von S3-, Web- und Admin-Server (server.rs:83 bzw. 124). Mit --single-node ist der Knoten also layout-fertig, bevor er überhaupt auf einem Port antwortet.

Und die Folge für die CI, die mir die wichtigste ist: mit --single-node erreicht der Pod von allein Ready, der Install-Test im PR wird dadurch aussagekräftig und schließt drei der vier offenen Messpunkte bei jedem PR. Ohne ihn kann kein Install-Test bestehen — eine ehrliche Readiness kann vor dem Layout von Hand nicht grün werden —, und die Fixture-Entscheidung müsste auf einen begründeten Ausschluss in ci/excluded-charts.txt hinauslaufen, der nichts belegt.

Mein Vorschlag: Wert garage.singleNode, Vorgabe true (RF 1 ist ohnehin gesetzt, und der Schalter verlangt genau das); --default-access-key/--default-bucket über eigene Werte dazu, gespeist aus demselben OnePasswordItem, das Stalwart liest. NOTES.txt behält die Handarbeits-Prüfliste für singleNode: false und führt im Vorgabefall die Tresor-Felder, die jetzt hineingehen statt heraus.


Was der Plan unverändert richtig hat (gegen die Quelle bestätigt)

  • metadata_snapshots_dir heißt genau so, ist oberste Ebene, Option<PathBuf>, Vorgabe <metadata_dir>/snapshots (src/util/config.rs:35, src/model/snapshot.rs:44-50).
  • Das zweite Volume trägt. Der Schnappschuss ist eine echte Kopie mit Kompaktierung (src/db/lmdb_adapter.rs:162-168: copy_to_path(path, CompactionOption::Enabled)) und das Zielverzeichnis wird mit create_dir_all angelegt (snapshot.rs:52). Kein rename, also kein EXDEV über die Dateisystemgrenze — ein eigenes PVC für die Schnappschüsse funktioniert.
  • Das Zwei-Schnappschuss-Fenster stimmt, mit einem Zusatz: KEEP_SNAPSHOTS = 2 (snapshot.rs:17), der erste nach interval/2 (AutoSnapshotWorker::new: now + interval/2), jeder weitere mit einem Zufallsfaktor 1,0–1,2 (snapshot.rs, rand_factor). Bei "6h" also erster Schnappschuss nach ~3 h, Fenster danach ~6–14,4 h statt 6–12 h.
  • metadata_dir und data_dir als zwei Unterverzeichnisse einer PVC geht, und Garage legt beide selbst an (src/model/garage.rs:100-104, create_dir_all). Das ist hier nicht nur bequem, sondern notwendig: das Abbild ist FROM scratch mit genau einer Datei, /garage (Upstream-Dockerfile). Es gibt keine Shell, kein mkdir — ein Init-Container wäre ein zweites Abbild für etwas, das die Anwendung selbst tut, und exec-Probes sind in diesem Chart grundsätzlich unmöglich.
  • Kein Ingress, keine Discovery-CRD, db_engine = "lmdb", metadata_fsync = true — unverändert.

Die Readiness-Frage ist aus der Quelle beantwortet

Der Plan fragt, ob die S3-API vor dem Layout antwortet. Sie tut es: der S3-Server wird unabhängig vom Layout-Zustand gestartet (server.rs:83-95), der Socket bindet also und antwortet — mit Fehlern. Eine TCP-Probe auf 3900 wäre damit genau die Lüge, vor der der Plan warnt.

Ehrlich ist /health auf dem Admin-Port: ohne Token (src/api/admin/router_v2.rs:31, @special), und src/rpc/system.rs:462-470 liefert für ein nicht konfiguriertes Layout den Sonderfall ClusterHealthStatus::Unavailable, den src/api/admin/special.rs:75-96 auf 503 abbildet („Quorum is not available for some/all partitions, reads and writes will fail"); Healthy und Degraded ergeben 200. Also: Readiness auf GET /health:3903, und Liveness und Startup ausdrücklich nicht — ein Knoten, der nur auf sein Layout wartet, würde sonst endlos neu gestartet (README, Leitlinie 12.3, derselbe Grund wie bei zipline). Die beiden gehen auf eine TCP-Probe des Admin-Ports.

Zwei Fragen, kurz zu beantworten

  1. RPC-Geheimnis: GARAGE_RPC_SECRET per secretKeyRef (a), oder rpc_secret_file mit allow_world_readable_secrets = true (b)?
  2. Erststart: garage.singleNode mit Vorgabe true samt --default-access-key/--default-bucket aus dem Tresor-Item — oder bleibt es bei Handarbeit nach NOTES.txt, und wenn ja, soll die Fixture-Entscheidung dann der Ausschluss in ci/excluded-charts.txt sein?

Bis dahin ist nichts gebaut, und der Branch ist nicht angelegt.

## Anhalten vor der Umsetzung: zwei Funde am freigegebenen Plan Gegen den Quellbaum von **v2.4.1** geprüft (`codeload.github.com/deuxfleurs-org/garage` am Tag `v2.4.1`, dem Spiegel des Deuxfleurs-Gitea — das Original liegt hinter Anubis und ist aus dem Workspace nicht abrufbar). Zwei Punkte des Plans halten der Quelle nicht stand, einer davon so, dass er in der vorgesehenen Form **nicht startet**. Gebaut ist noch nichts. --- ### Fund 1 — `rpc_secret_file` aus einem Kubernetes-Secret ist im gehärteten Pod nicht lesbar Der Plan hält fest: „`rpc_secret_file` **existiert** — kommt als Datei aus einem `OnePasswordItem`, nicht in die ConfigMap". Der Schlüssel existiert (`src/util/config.rs:96`), aber die Datei kann unser Container nicht lesen. Das ist keine Konfigurationsfrage, es sind zwei Prüfungen, die sich widersprechen: **Garage, `src/garage/secrets.rs:135-147`:** ```rust if metadata.mode() & 0o077 != 0 { return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)\n\ Refusing to start until this is fixed, or environment variable \ GARAGE_ALLOW_WORLD_READABLE_SECRETS is set to true.", ...).into()); } ``` Die Maske ist `0o077` — sie verwirft **jedes** Gruppen- *oder* Fremdbit, nicht nur die Fremdbits, trotz des Wortes „world-readable" in der Meldung. **Das Kubelet, `pkg/volume/volume_linux.go:72-104`** (geprüft an `release-1.31`): sobald `fsGroup` gesetzt ist, läuft über jede Datei des Volumes ```go err := os.Lchown(filename, -1, int(*fsGroup)) mask := rwMask // 0660 if readonly { mask = roMask } // 0440 — Secret-Volumes melden ReadOnly: true err = os.Chmod(filename, info.Mode()|mask) ``` Das Gruppen-Lesebit wird also **hinzu-ge-ODERt**, unabhängig von `defaultMode`. Damit ist `mode & 0o077 != 0` immer wahr und Garage verweigert den Start. Und es gibt keine Kombination, die beides erfüllt, weil Kubernetes an einer Secret-Volume-Datei nur die **Gruppe** setzen kann, niemals den besitzenden Benutzer: | Variante | Modus / Eigentümer | Ergebnis | | --- | --- | --- | | `fsGroup` gesetzt, `defaultMode` beliebig | `root:1000`, Gruppenbit immer gesetzt | Garage verweigert den Start (`0o077`-Prüfung) | | kein `fsGroup`, `defaultMode: 0400` | `root:root 0400` | Prüfung besteht, aber uid 1000 bekommt `EACCES` — „Failed to read secret file" | | kein `fsGroup`, Vorgabe `0644` | `root:root 0644` | Garage verweigert den Start | | `runAsUser: 0` | — | widerspricht der Härtung | **Zwei Wege hinaus, und sie sind nicht gleichwertig:** **(a) `GARAGE_RPC_SECRET` als `env.valueFrom.secretKeyRef`.** `src/garage/secrets.rs:22-28` führt den Wert mit `env = "GARAGE_RPC_SECRET"`, und `src/garage/main.rs:50` flacht `Secrets` in die **globalen** Optionen ein — die Variable gilt also für `garage server` genauso wie für `kubectl exec … -- /garage status`, das dieselbe Berechtigung braucht. Keine Datei, keine Dateirechte, keine abgeschaltete Prüfung, und das eigentliche Ziel des Plans ist erfüllt: das Geheimnis bleibt ein Secret und landet **nicht** in der ConfigMap. **(b) `rpc_secret_file` + `allow_world_readable_secrets = true`.** Funktioniert, kostet aber die Prüfung für **alle** Geheimnisdateien dieses Prozesses — und die Datei ist dabei gar nicht fremdlesbar, Garage kann es nur nicht unterscheiden. Upstream kennt den Fall übrigens: der Doc-Kommentar am Wert lautet „Skip the permission check of secret files. Useful when POSIX ACLs (or more complex chmods) are used." (`src/util/config.rs:86-89`) — der Schalter ist für genau diese Lage gedacht, nur eben pauschal. **Mein Vorschlag: (a)**, mit dem Mechanismus als Kommentar an `secrets.rpcSecretRef` in `values.yaml`, plus ein `Lessons-Learned`-Eintrag zur allgemeinen Form: *eine Rechteprüfung, die annimmt, ihr Dateisystem zu besitzen, trifft auf eine Plattform, die nur die Gruppe setzen kann* — das wiederholt sich bei jedem Programm mit `*_secret_file` und einem `0o077`-Test (Garage selbst hat drei davon: `rpc_secret_file`, `admin_token_file`, `metrics_token_file`). --- ### Fund 2 — `garage server --single-node` macht genau das, wofür Entscheidung 5 Handarbeit vorsieht, und entkräftet beide Begründungen Entscheidung 5 steht auf zwei Prämissen. Beide treffen für v2.4.1 bei einem Knoten nicht mehr zu. **Die Schalter, `src/garage/cli/structs.rs:95-108`:** ```rust pub struct ServerOpt { /// Automatically configure a single-node layout in the cluster. /// Garage will refuse to run if the cluster already has other nodes. #[structopt(long = "single-node")] pub(crate) single_node: bool, /// Configure a default S3 API key using environment variables /// `GARAGE_DEFAULT_ACCESS_KEY` and `GARAGE_DEFAULT_SECRET_KEY`. Requires `--single-node`. #[structopt(long = "default-access-key")] pub(crate) default_access_key: bool, /// Configure a default bucket using environment variable `GARAGE_DEFAULT_BUCKET`. /// Implies `--default-access-key`. Requires `--single-node`. #[structopt(long = "default-bucket")] pub(crate) default_bucket: bool, } ``` **Prämisse 1 („`layout apply --version N+1` ist nicht idempotent, ein Job bei `helm upgrade` ist gefährlich") fällt.** `initial_config` in `src/garage/server.rs:190-256` prüft die Layout-Version **im Prozess**, nicht über einen wiederholbaren CLI-Aufruf: - `replication_factor != 1` → Abbruch mit „Single-node mode requires replication_factor = 1 in the configuration file." (genau unsere Konfiguration, also kein Hindernis) - Layout-Version `0` → Layout wird angelegt: Zone `dc1`, Kapazität aus der erkannten Plattengröße, Tag `default`, dann `apply_staged_changes(1)` - Layout-Version `1` → **nichts passiert**, der Start läuft normal weiter - Layout-Version `> 1` → Abbruch mit „Refusing to run in single-node mode: layout version is already superior to 1. Remove the --single-node flag to run the server in full mode." - fremde Knoten bekannt → Abbruch mit „Refusing to run in single-node mode: more nodes are already present in the cluster." Das ist über Neustarts hinweg idempotent, und die Falle, vor der der Plan zu Recht warnt, entsteht gar nicht: es gibt keinen `--version`-Parameter, den jemand zweimal mit demselben Wert aufrufen könnte. **Prämisse 2 („Der Zugangsschlüssel, den Garage erzeugt, muss in das 1Password-Item wandern. Das kann kein Job.") fällt und kehrt sich um.** `--default-access-key` liest `GARAGE_DEFAULT_ACCESS_KEY` und `GARAGE_DEFAULT_SECRET_KEY` aus der Umgebung und **importiert** den Schlüssel (`server.rs:259-300`, `Key::import` in Zeile 291). Die Richtung ist damit die bessere: der Betreuer erzeugt das Schlüsselpaar im Tresor, das Item ist die Quelle der Wahrheit, und Garage übernimmt es — statt dass Garage es erzeugt und es von Hand hinterherkopiert wird. Der Wiederholungsfall ist in der Quelle abgesichert: existierender Schlüssel mit **anderem** Geheimnis → Fehler; im Cluster gelöschter Schlüssel → Fehler („cannot add it back"). `--default-bucket` legt zusätzlich den Bucket an und bindet den Schlüssel daran. **Dazu die Reihenfolge, die für die Readiness-Entscheidung zählt:** `initial_config(&garage, opt).await?` steht in `server.rs:54` — **vor** dem Start von S3-, Web- und Admin-Server (`server.rs:83` bzw. `124`). Mit `--single-node` ist der Knoten also layout-fertig, bevor er überhaupt auf einem Port antwortet. **Und die Folge für die CI, die mir die wichtigste ist:** mit `--single-node` erreicht der Pod von allein `Ready`, der Install-Test im PR wird dadurch **aussagekräftig** und schließt drei der vier offenen Messpunkte bei *jedem* PR. Ohne ihn kann kein Install-Test bestehen — eine ehrliche Readiness kann vor dem Layout von Hand nicht grün werden —, und die Fixture-Entscheidung müsste auf einen begründeten Ausschluss in `ci/excluded-charts.txt` hinauslaufen, der nichts belegt. **Mein Vorschlag:** Wert `garage.singleNode`, Vorgabe `true` (RF 1 ist ohnehin gesetzt, und der Schalter verlangt genau das); `--default-access-key`/`--default-bucket` über eigene Werte dazu, gespeist aus demselben `OnePasswordItem`, das Stalwart liest. `NOTES.txt` behält die Handarbeits-Prüfliste für `singleNode: false` und führt im Vorgabefall die Tresor-Felder, die jetzt **hinein**gehen statt heraus. --- ### Was der Plan unverändert richtig hat (gegen die Quelle bestätigt) - **`metadata_snapshots_dir` heißt genau so**, ist oberste Ebene, `Option<PathBuf>`, Vorgabe `<metadata_dir>/snapshots` (`src/util/config.rs:35`, `src/model/snapshot.rs:44-50`). - **Das zweite Volume trägt.** Der Schnappschuss ist eine echte Kopie mit Kompaktierung (`src/db/lmdb_adapter.rs:162-168`: `copy_to_path(path, CompactionOption::Enabled)`) und das Zielverzeichnis wird mit `create_dir_all` angelegt (`snapshot.rs:52`). Kein `rename`, also kein `EXDEV` über die Dateisystemgrenze — ein eigenes PVC für die Schnappschüsse funktioniert. - **Das Zwei-Schnappschuss-Fenster stimmt**, mit einem Zusatz: `KEEP_SNAPSHOTS = 2` (`snapshot.rs:17`), der erste nach `interval/2` (`AutoSnapshotWorker::new`: `now + interval/2`), jeder weitere mit einem Zufallsfaktor 1,0–1,2 (`snapshot.rs`, `rand_factor`). Bei `"6h"` also erster Schnappschuss nach ~3 h, Fenster danach ~6–14,4 h statt 6–12 h. - **`metadata_dir` und `data_dir` als zwei Unterverzeichnisse *einer* PVC geht**, und Garage legt beide selbst an (`src/model/garage.rs:100-104`, `create_dir_all`). Das ist hier nicht nur bequem, sondern notwendig: das Abbild ist **`FROM scratch`** mit genau einer Datei, `/garage` (Upstream-`Dockerfile`). Es gibt keine Shell, kein `mkdir` — ein Init-Container wäre ein zweites Abbild für etwas, das die Anwendung selbst tut, und `exec`-Probes sind in diesem Chart grundsätzlich unmöglich. - **Kein Ingress, keine Discovery-CRD, `db_engine = "lmdb"`, `metadata_fsync = true`** — unverändert. ### Die Readiness-Frage ist aus der Quelle beantwortet Der Plan fragt, ob die S3-API vor dem Layout antwortet. Sie tut es: der S3-Server wird unabhängig vom Layout-Zustand gestartet (`server.rs:83-95`), der Socket bindet also und antwortet — mit Fehlern. Eine TCP-Probe auf 3900 wäre damit genau die Lüge, vor der der Plan warnt. Ehrlich ist `/health` auf dem Admin-Port: ohne Token (`src/api/admin/router_v2.rs:31`, `@special`), und `src/rpc/system.rs:462-470` liefert für ein nicht konfiguriertes Layout den Sonderfall `ClusterHealthStatus::Unavailable`, den `src/api/admin/special.rs:75-96` auf **503** abbildet („Quorum is not available for some/all partitions, reads and writes will fail"); `Healthy` und `Degraded` ergeben 200. Also: **Readiness auf `GET /health:3903`**, und **Liveness und Startup ausdrücklich nicht** — ein Knoten, der nur auf sein Layout wartet, würde sonst endlos neu gestartet (README, Leitlinie 12.3, derselbe Grund wie bei `zipline`). Die beiden gehen auf eine TCP-Probe des Admin-Ports. ### Zwei Fragen, kurz zu beantworten 1. **RPC-Geheimnis:** `GARAGE_RPC_SECRET` per `secretKeyRef` (a), oder `rpc_secret_file` mit `allow_world_readable_secrets = true` (b)? 2. **Erststart:** `garage.singleNode` mit Vorgabe `true` samt `--default-access-key`/`--default-bucket` aus dem Tresor-Item — oder bleibt es bei Handarbeit nach `NOTES.txt`, und wenn ja, soll die Fixture-Entscheidung dann der Ausschluss in `ci/excluded-charts.txt` sein? Bis dahin ist nichts gebaut, und der Branch ist nicht angelegt.
Author
Member

Entscheidungen zu den beiden Funden — der Plan wird an zwei Stellen korrigiert

Beide Funde sind gegen den Quelltext am Tag v2.4.1 nachgeprüft und tragen.

Fund 1, src/garage/secrets.rs:135-146:

if metadata.mode() & 0o077 != 0 {
    return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)…

Die Maske verwirft jedes Gruppen- oder Fremdbit; die Meldung sagt „world-readable", geprüft wird aber auch die Gruppe. fsGroup setzt genau dieses Gruppen-Lesebit — ohne es kann uid 1000 die Datei nicht lesen, mit ihm verweigert Garage den Start. Kubernetes kann an einer Secret-Datei nur die Gruppe setzen, nie den Eigentümer, also existiert keine funktionierende Kombination.

Fund 2, src/garage/cli/structs.rs:95-108: --single-node, --default-access-key (GARAGE_DEFAULT_ACCESS_KEY/GARAGE_DEFAULT_SECRET_KEY), --default-bucket (GARAGE_DEFAULT_BUCKET), mit dem dokumentierten Schutz „Garage will refuse to run if the cluster already has other nodes."

Entscheidung 1: GARAGE_RPC_SECRET als Umgebungsvariable

Per valueFrom.secretKeyRef aus dem OnePasswordItem. Das eigentliche Ziel des Plans — nichts in der ConfigMap — ist damit erfüllt, und keine Prüfung wird abgeschaltet. Es ist das überwiegende Muster dieses Repositories; paperless-ngx und stalwart holen ihre Geheimnisse genauso.

Der Preis gehört an den Wert geschrieben: das Geheimnis steht in der Prozessumgebung des Containers. Unser valkey-Chart argumentiert ausdrücklich dagegen („the password never appears in the process list, in an environment variable"). Dass es hier anders ausfällt, hat einen Grund und der gehört dokumentiert: bei valkey war die Datei ohnehin der natürliche Weg, weil die Konfiguration sie includet; hier ist der Dateiweg strukturell versperrt, nicht nur unbequem. Garage führt den Wert mit hide_env_values = true, gibt ihn also in eigenen Ausgaben nicht preis.

Verworfen:

  • allow_world_readable_secrets = true — würde die Prüfung für alle drei Geheimnisdateien aufheben (rpc_secret_file, admin_token_file, metrics_token_file), auch für künftige, die noch niemand gesetzt hat. Eine Sicherheitsprüfung abzuschalten, um ein Berechtigungsmodell zu bedienen, ist der schlechtere Tausch.
  • Init-Container, der die Datei mit Modus 0600 kopiert — technisch sauber und die Prüfung bliebe aktiv, aber das Hauptabbild ist FROM scratch ohne Shell, der Init bräuchte also ein eigenes Abbild zum Pinnen und Verfolgen, plus ein emptyDir, in dem ein Geheimnis liegt.

Entscheidung 2: --single-node statt Handarbeit — mein Plan war an dieser Stelle falsch begründet

Ich hatte „Erststart von Hand" mit zwei Gründen belegt, und beide fallen:

  1. „layout apply ist nicht idempotent" gilt für den CLI-Befehl, aber --single-node nimmt diesen Weg nicht: es prüft die Layout-Version im Prozess und hat keinen --version-Parameter, der zweimal gleich aufgerufen werden könnte. Voraussetzung ist RF 1 — genau unsere Konfiguration.
  2. „Den Zugangsschlüssel kann kein Job besorgen" kehrt sich um: --default-access-key importiert den Schlüssel aus der Umgebung. Das Tresor-Item wird damit Quelle der Wahrheit statt Ablage für etwas, das hinterher hineinkopiert wird.

Entscheidend ist die Folge für die Prüfbarkeit: initial_config läuft vor dem S3- und dem Admin-Server, der Pod erreicht also von allein Ready. Damit wird der Install-Test aussagekräftig und schließt drei der vier Messpunkte bei jedem PR. Ohne das Flag kann kein Install-Test bestehen, und die Fixture-Entscheidung wäre auf einen Ausschluss hinausgelaufen, der nichts belegt.

Umsetzung: garage.singleNode als Wert mit Vorgabe true — ein Standard, der nicht Ready wird, ist kein Standard. --default-access-key und --default-bucket werden mitgeschaltet, die vier Tresor-Felder (RPC-Geheimnis, Access Key, Secret Key, Bucket-Name) stehen in NOTES.txt. Der Schutz „refuse to run if the cluster already has other nodes" gehört als Zusicherung dazu: das Flag kann einen bestehenden Cluster nicht beschädigen.

Damit entfällt die Fixture-Frage: ci/garage/ wird gebaut, kein Ausschluss.

Entscheidung 3: die Probes, wie hergeleitet

Die S3-API bindet vor dem Layout (server.rs:83-95), eine TCP-Probe auf 3900 wäre also genau die Lüge, vor der der Plan warnte.

  • Readiness auf GET /health an Port 3903 — ohne Token, 503 bei unkonfiguriertem Layout, 200 bei Healthy/Degraded.
  • Liveness und Startup ausdrücklich nicht darauf, sondern TCP auf den Admin-Port. Ein Knoten, der auf sein Layout wartet, würde sonst endlos neu gestartet — dieselbe Lehre wie bei zipline, README 12.3.

Was mitkorrigiert wird

  • Das Abbild ist FROM scratch mit genau einer Datei /garage. Keine Shell: exec-Probes sind in diesem Chart grundsätzlich unmöglich, und jeder Init-Container bräuchte ein eigenes Abbild. Gehört in values.yaml und auf die Wiki-Seite, weil es jede künftige Runde betrifft.
  • Schnappschuss-Fenster: erster nach interval/2, danach Zufallsfaktor 1,0–1,2, KEEP_SNAPSHOTS = 2. Bei "6h" also ~3 h und danach ~6–14,4 h — die Angabe „6–12 h" im Plan war zu eng.
  • metadata_snapshots_dir heißt genau so, oberste Ebene, Vorgabe <metadata_dir>/snapshots — das zweite PVC bleibt richtig, und der Schnappschuss ist eine echte Kopie mit Kompaktierung, also kein rename über eine Dateisystemgrenze.
  • metadata_dir und data_dir als Unterverzeichnisse einer PVC trägt; Garage legt beide selbst an.
  • Upstreams eigener Chart pinnt dxflrs/amd64_garage; das im Issue verifizierte dxflrs/garage ist das Multi-Arch-Abbild und bleibt richtig.

Alles Übrige aus dem Plan-Kommentar bleibt unverändert gültig: RF 1 mit benanntem Restrisiko, lmdb mit metadata_fsync = true und Auto-Schnappschuss, zwei PVCs in der beschriebenen Aufteilung, kein Ingress, keine Discovery-CRD.

# Entscheidungen zu den beiden Funden — der Plan wird an zwei Stellen korrigiert Beide Funde sind gegen den Quelltext am Tag `v2.4.1` nachgeprüft und tragen. **Fund 1**, `src/garage/secrets.rs:135-146`: ```rust if metadata.mode() & 0o077 != 0 { return Err(format!("File {} is world-readable! (mode: 0{:o}, expected 0600)… ``` Die Maske verwirft **jedes** Gruppen- *oder* Fremdbit; die Meldung sagt „world-readable", geprüft wird aber auch die Gruppe. `fsGroup` setzt genau dieses Gruppen-Lesebit — ohne es kann uid 1000 die Datei nicht lesen, mit ihm verweigert Garage den Start. Kubernetes kann an einer Secret-Datei nur die **Gruppe** setzen, nie den Eigentümer, also existiert keine funktionierende Kombination. **Fund 2**, `src/garage/cli/structs.rs:95-108`: `--single-node`, `--default-access-key` (`GARAGE_DEFAULT_ACCESS_KEY`/`GARAGE_DEFAULT_SECRET_KEY`), `--default-bucket` (`GARAGE_DEFAULT_BUCKET`), mit dem dokumentierten Schutz „Garage will refuse to run if the cluster already has other nodes." ## Entscheidung 1: `GARAGE_RPC_SECRET` als Umgebungsvariable Per `valueFrom.secretKeyRef` aus dem `OnePasswordItem`. Das eigentliche Ziel des Plans — **nichts in der ConfigMap** — ist damit erfüllt, und keine Prüfung wird abgeschaltet. Es ist das überwiegende Muster dieses Repositories; `paperless-ngx` und `stalwart` holen ihre Geheimnisse genauso. **Der Preis gehört an den Wert geschrieben:** das Geheimnis steht in der Prozessumgebung des Containers. Unser `valkey`-Chart argumentiert ausdrücklich dagegen („the password never appears in the process list, in an environment variable"). Dass es hier anders ausfällt, hat einen Grund und der gehört dokumentiert: bei `valkey` war die Datei ohnehin der natürliche Weg, weil die Konfiguration sie `include`t; hier ist der Dateiweg **strukturell versperrt**, nicht nur unbequem. Garage führt den Wert mit `hide_env_values = true`, gibt ihn also in eigenen Ausgaben nicht preis. Verworfen: - **`allow_world_readable_secrets = true`** — würde die Prüfung für **alle drei** Geheimnisdateien aufheben (`rpc_secret_file`, `admin_token_file`, `metrics_token_file`), auch für künftige, die noch niemand gesetzt hat. Eine Sicherheitsprüfung abzuschalten, um ein Berechtigungsmodell zu bedienen, ist der schlechtere Tausch. - **Init-Container, der die Datei mit Modus 0600 kopiert** — technisch sauber und die Prüfung bliebe aktiv, aber das Hauptabbild ist `FROM scratch` ohne Shell, der Init bräuchte also ein **eigenes Abbild** zum Pinnen und Verfolgen, plus ein emptyDir, in dem ein Geheimnis liegt. ## Entscheidung 2: `--single-node` statt Handarbeit — mein Plan war an dieser Stelle falsch begründet Ich hatte „Erststart von Hand" mit zwei Gründen belegt, und **beide fallen**: 1. „`layout apply` ist nicht idempotent" gilt für den CLI-Befehl, aber `--single-node` nimmt diesen Weg nicht: es prüft die Layout-Version im Prozess und hat **keinen `--version`-Parameter**, der zweimal gleich aufgerufen werden könnte. Voraussetzung ist RF 1 — genau unsere Konfiguration. 2. „Den Zugangsschlüssel kann kein Job besorgen" kehrt sich um: `--default-access-key` **importiert** den Schlüssel aus der Umgebung. Das Tresor-Item wird damit **Quelle der Wahrheit** statt Ablage für etwas, das hinterher hineinkopiert wird. Entscheidend ist die Folge für die Prüfbarkeit: `initial_config` läuft **vor** dem S3- und dem Admin-Server, der Pod erreicht also von allein `Ready`. Damit wird der **Install-Test aussagekräftig** und schließt drei der vier Messpunkte bei jedem PR. Ohne das Flag kann kein Install-Test bestehen, und die Fixture-Entscheidung wäre auf einen Ausschluss hinausgelaufen, der nichts belegt. Umsetzung: `garage.singleNode` als Wert mit Vorgabe **`true`** — ein Standard, der nicht `Ready` wird, ist kein Standard. `--default-access-key` und `--default-bucket` werden mitgeschaltet, die vier Tresor-Felder (RPC-Geheimnis, Access Key, Secret Key, Bucket-Name) stehen in `NOTES.txt`. Der Schutz „refuse to run if the cluster already has other nodes" gehört als Zusicherung dazu: das Flag kann einen bestehenden Cluster nicht beschädigen. **Damit entfällt die Fixture-Frage:** `ci/garage/` wird gebaut, kein Ausschluss. ## Entscheidung 3: die Probes, wie hergeleitet Die S3-API bindet **vor** dem Layout (`server.rs:83-95`), eine TCP-Probe auf 3900 wäre also genau die Lüge, vor der der Plan warnte. - **Readiness** auf `GET /health` an Port 3903 — ohne Token, 503 bei unkonfiguriertem Layout, 200 bei `Healthy`/`Degraded`. - **Liveness und Startup ausdrücklich nicht darauf**, sondern TCP auf den Admin-Port. Ein Knoten, der auf sein Layout wartet, würde sonst endlos neu gestartet — dieselbe Lehre wie bei `zipline`, README 12.3. ## Was mitkorrigiert wird - Das Abbild ist **`FROM scratch`** mit genau einer Datei `/garage`. Keine Shell: **`exec`-Probes sind in diesem Chart grundsätzlich unmöglich**, und jeder Init-Container bräuchte ein eigenes Abbild. Gehört in `values.yaml` und auf die Wiki-Seite, weil es jede künftige Runde betrifft. - Schnappschuss-Fenster: erster nach `interval/2`, danach Zufallsfaktor 1,0–1,2, `KEEP_SNAPSHOTS = 2`. Bei `"6h"` also ~3 h und danach ~6–14,4 h — die Angabe „6–12 h" im Plan war zu eng. - `metadata_snapshots_dir` heißt genau so, oberste Ebene, Vorgabe `<metadata_dir>/snapshots` — das zweite PVC bleibt richtig, und der Schnappschuss ist eine echte Kopie mit Kompaktierung, also kein `rename` über eine Dateisystemgrenze. - `metadata_dir` und `data_dir` als Unterverzeichnisse **einer** PVC trägt; Garage legt beide selbst an. - Upstreams eigener Chart pinnt `dxflrs/amd64_garage`; das im Issue verifizierte `dxflrs/garage` ist das Multi-Arch-Abbild und bleibt richtig. Alles Übrige aus dem Plan-Kommentar bleibt unverändert gültig: RF 1 mit benanntem Restrisiko, `lmdb` mit `metadata_fsync = true` und Auto-Schnappschuss, zwei PVCs in der beschriebenen Aufteilung, kein Ingress, keine Discovery-CRD.
Author
Member

Stand nach dem Merge (2026-10-04)

garage/ liegt auf main, Version 1.0.0+upv2.4.1, veröffentlicht. PR #82 gemergt am 2026-10-04 um 15:09 mit Part of #81 — dieses Issue ist deshalb bewusst offen.

In der CI belegt (Lauf 447, alle sechs Kontexte grün): der Container execed mit drop: [ALL] + add: [NET_BIND_SERVICE] gegen das setcap +ep des Abbilds — der capabilities(7)-Schluss war vorher nur aus der Spezifikation abgeleitet, jetzt ist er beobachtet; /health wird auf dem Recovery-Listener bedient und antwortet einer Kubelet-Probe; readOnlyRootFilesystem: true hält mit den drei emptyDirs; die authSecret-Indirektion über eine Umgebungsvariable löst auf, das Passwort steht nirgends in der ConfigMap.

Offen bleibt eine Prüfung, und sie ist nicht CI-schließbar:

Landet der Metadaten-Schnappschuss wirklich in metadata_snapshots_dir? Der erste ist nach interval/2 fällig — länger als jeder CI-Lauf, und ein Intervall von Sekunden würde den Timer prüfen statt den Mechanismus. Schritt 6 der Prüfliste auf der Wiki-Seite.

Das ist der Punkt, an dem die Sicherungsgeschichte hängt: Dateisystem-Schnappschüsse von metadata_dir sind kein gültiger Rücksetzpunkt (wird während eines Schreibvorgangs geschnappschosst, ist der Schnappschuss selbst beschädigt), deshalb liegt der konsistente Rücksetzpunkt auf dem zweiten Volume.

Teilweise offen: Longhorns Schnappschuss-Semantik, auf der die Ein-PVC-Aufteilung ruht, ist eine Eigenschaft der Speicherschicht — der Runner hat local-path. Die Haltbarkeit von metadata_fsync ist im Container grundsätzlich nicht beobachtbar. Und es hat nirgends ein echter S3-Schreibvorgang stattgefunden: dass Bucket und Zugangsschlüssel existieren, folgt aus dem Start, dass ein PUT durchgeht, ist ungeprüft.

## Stand nach dem Merge (2026-10-04) `garage/` liegt auf `main`, Version **1.0.0+upv2.4.1**, veröffentlicht. PR #82 gemergt am 2026-10-04 um 15:09 mit `Part of #81` — dieses Issue ist deshalb bewusst **offen**. **In der CI belegt** (Lauf 447, alle sechs Kontexte grün): der Container **execed** mit `drop: [ALL]` + `add: [NET_BIND_SERVICE]` gegen das `setcap +ep` des Abbilds — der `capabilities(7)`-Schluss war vorher nur aus der Spezifikation abgeleitet, jetzt ist er beobachtet; `/health` wird **auf dem Recovery-Listener** bedient und antwortet einer Kubelet-Probe; `readOnlyRootFilesystem: true` hält mit den drei emptyDirs; die `authSecret`-Indirektion über eine Umgebungsvariable löst auf, das Passwort steht nirgends in der ConfigMap. **Offen bleibt eine Prüfung, und sie ist nicht CI-schließbar:** **Landet der Metadaten-Schnappschuss wirklich in `metadata_snapshots_dir`?** Der erste ist nach `interval/2` fällig — länger als jeder CI-Lauf, und ein Intervall von Sekunden würde den Timer prüfen statt den Mechanismus. Schritt 6 der Prüfliste auf der Wiki-Seite. Das ist der Punkt, an dem die Sicherungsgeschichte hängt: Dateisystem-Schnappschüsse von `metadata_dir` sind **kein** gültiger Rücksetzpunkt (wird während eines Schreibvorgangs geschnappschosst, ist der Schnappschuss selbst beschädigt), deshalb liegt der konsistente Rücksetzpunkt auf dem zweiten Volume. **Teilweise offen:** Longhorns Schnappschuss-Semantik, auf der die Ein-PVC-Aufteilung ruht, ist eine Eigenschaft der Speicherschicht — der Runner hat `local-path`. Die Haltbarkeit von `metadata_fsync` ist im Container grundsätzlich nicht beobachtbar. Und **es hat nirgends ein echter S3-Schreibvorgang stattgefunden**: dass Bucket und Zugangsschlüssel *existieren*, folgt aus dem Start, dass ein `PUT` durchgeht, ist ungeprüft.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jlab-cluster/helm-charts#81