docs: Konfigurationsvorlagen vollstaendig und uebernahmefaehig machen
01-configmap.yaml und 02-secret.example.yaml sind jetzt so geschrieben, dass sie sich als Produktivkonfiguration uebernehmen lassen: in drei Abschnitte gegliedert (ANPASSEN / Vorgaben / optional), jeder Wert mit der Angabe, was ohne ihn nicht funktioniert. Anzupassen sind genau zwei Werte in der ConfigMap und die mit "CHANGE-ME" markierten im Secret; alles Uebrige traegt eine brauchbare Vorgabe oder darf leer bleiben. Beide Dateien decken zusammen jede Umgebungsvariable ab, die die Anwendung liest, und jeder Schluessel steht in genau einer der beiden. Bisher war das Handarbeit und driftete: GOOGLE_PLACES_API_KEY lag in der unverschluesselten ConfigMap, APICORE_API_URL unnoetig im Secret. Neu ist deshalb src/lib/k8s-manifest-env.test.ts. Der Test liest die process.env-Zugriffe unter src/ und die Schluessel der beiden Manifeste und prueft vier Zusagen: keine gelesene Variable fehlt, kein Schluessel steht doppelt (sonst entschiede allein die Reihenfolge der envFrom-Eintraege, welcher Wert gewinnt), kein Schluessel ist tot, und die committete Secret-Vorlage enthaelt keine ausgefuellten Werte. Beim ersten Lauf hat der Test zwei Dinge gefunden. ADMIN_EMAIL und ADMIN_PASSWORD liest nicht die App, sondern prisma/seed.ts — das ist legitim und steht jetzt begruendet in einer Ausnahmeliste, zusammen mit DB_PASSWORD fuer das Datenbank-StatefulSet. STRIPE_PUBLISHABLE_KEY dagegen wird von keiner einzigen Code-Stelle gelesen: der Checkout laeuft vollstaendig serverseitig ueber eine Stripe-Checkout-Session. Der Schluessel ist aus Secret-Vorlage, .env.example, docker-compose.yml und README entfernt, mit Hinweis, wofuer man ihn braeuchte. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
990a16e654
commit
479ace1b88
6 files changed
+323
-87
No files matched your search
@@ -0,0 +1,129 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Haelt die Kubernetes-Manifeste und den Code in Deckung.
|
||||
*
|
||||
* Die Manifeste unter k8s/ sind ausdruecklich dafuer gedacht, mit moeglichst
|
||||
* wenig Anpassung als Produktivkonfiguration uebernommen zu werden. Das
|
||||
* traegt nur, solange sie VOLLSTAENDIG sind. Ohne diese Pruefung faellt eine
|
||||
* im Code neu hinzugekommene Umgebungsvariable erst im Betrieb auf, und zwar
|
||||
* als stiller Rueckfall auf einen Standardwert statt als Fehler: zur Laufzeit
|
||||
* ist eine fehlende Variable nicht von einer absichtlich leeren zu
|
||||
* unterscheiden.
|
||||
*
|
||||
* Bewusst KEIN YAML-Parser: das Projekt hat keinen als Abhaengigkeit, und
|
||||
* fuer diesen Zweck genuegt es, die Schluesselzeilen der flachen
|
||||
* data:/stringData:-Bloecke zu lesen. Die Manifeste sind von Hand gepflegt
|
||||
* und halten dieses einfache Format ein.
|
||||
*/
|
||||
|
||||
const REPO_ROOT = process.cwd();
|
||||
const CONFIGMAP = path.join(REPO_ROOT, "k8s", "01-configmap.yaml");
|
||||
const SECRET = path.join(REPO_ROOT, "k8s", "02-secret.example.yaml");
|
||||
const SRC = path.join(REPO_ROOT, "src");
|
||||
|
||||
/**
|
||||
* Schluessel eines Manifests: genau zwei Leerzeichen eingerueckt, beginnend
|
||||
* mit einem Grossbuchstaben. Kommentarzeilen (" # ...") und Metadatenfelder
|
||||
* (" name: ...") fallen dadurch heraus, ohne Sonderfaelle.
|
||||
*/
|
||||
function manifestKeys(file: string): string[] {
|
||||
return readFileSync(file, "utf8")
|
||||
.split("\n")
|
||||
.flatMap((line) => {
|
||||
const match = /^ {2}([A-Z][A-Z0-9_]*):/.exec(line);
|
||||
return match ? [match[1]] : [];
|
||||
});
|
||||
}
|
||||
|
||||
/** Alle process.env-Zugriffe unterhalb von src/ — das ist die Laufzeit der App. */
|
||||
function envNamesUsedInCode(dir: string): Set<string> {
|
||||
const found = new Set<string>();
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
for (const name of envNamesUsedInCode(full)) found.add(name);
|
||||
continue;
|
||||
}
|
||||
if (!/\.tsx?$/.test(entry.name) || entry.name.endsWith(".test.ts")) continue;
|
||||
for (const match of readFileSync(full, "utf8").matchAll(/process\.env\.([A-Z][A-Z0-9_]*)/g)) {
|
||||
found.add(match[1]);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schluessel, die in den Manifesten stehen duerfen, ohne dass die App sie
|
||||
* liest. Wer hier etwas ergaenzt, muss benennen koennen, WER den Wert sonst
|
||||
* liest — sonst sammelt sich Konfiguration an, die nichts mehr tut.
|
||||
*/
|
||||
const USED_OUTSIDE_THE_APP: Record<string, string> = {
|
||||
DB_PASSWORD:
|
||||
"Datenbank-StatefulSet (03-postgres.yaml/03-mysql.yaml) und Sicherung (07-backup-cronjob.yaml); die App kennt nur DATABASE_URL.",
|
||||
ADMIN_EMAIL: "prisma/seed.ts — legt beim Start das erste Admin-Konto an.",
|
||||
ADMIN_PASSWORD: "prisma/seed.ts — legt beim Start das erste Admin-Konto an.",
|
||||
};
|
||||
|
||||
describe("Kubernetes-Manifeste und Code", () => {
|
||||
test("die Manifeste liefern jede Variable, die die App liest", () => {
|
||||
// Arrange
|
||||
const provided = new Set([...manifestKeys(CONFIGMAP), ...manifestKeys(SECRET)]);
|
||||
|
||||
// Act
|
||||
const missing = [...envNamesUsedInCode(SRC)].filter((name) => !provided.has(name)).sort();
|
||||
|
||||
// Assert
|
||||
expect(missing, `In k8s/ fehlen: ${missing.join(", ")}`).toEqual([]);
|
||||
});
|
||||
|
||||
test("kein Schluessel steht doppelt in ConfigMap und Secret", () => {
|
||||
// Arrange
|
||||
const config = manifestKeys(CONFIGMAP);
|
||||
const secret = new Set(manifestKeys(SECRET));
|
||||
|
||||
// Act
|
||||
const inBoth = config.filter((name) => secret.has(name)).sort();
|
||||
|
||||
// Assert: stuenden sie in beiden, entschiede allein die Reihenfolge der
|
||||
// envFrom-Eintraege, welcher Wert gewinnt — eine Falle beim Suchen.
|
||||
expect(inBoth, `Doppelt vergeben: ${inBoth.join(", ")}`).toEqual([]);
|
||||
});
|
||||
|
||||
test("kein Schluessel in den Manifesten, den niemand liest", () => {
|
||||
// Arrange
|
||||
const used = envNamesUsedInCode(SRC);
|
||||
|
||||
// Act
|
||||
const unused = [...manifestKeys(CONFIGMAP), ...manifestKeys(SECRET)]
|
||||
.filter((name) => !used.has(name) && !(name in USED_OUTSIDE_THE_APP))
|
||||
.sort();
|
||||
|
||||
// Assert: toter Konfigurationsballast ist irrefuehrend — er sieht aus,
|
||||
// als liesse sich damit etwas einstellen.
|
||||
expect(unused, `Wird nirgends gelesen: ${unused.join(", ")}`).toEqual([]);
|
||||
});
|
||||
|
||||
test("die Secret-Vorlage enthaelt keine ausgefuellten Werte", () => {
|
||||
// Arrange
|
||||
const secret = readFileSync(SECRET, "utf8");
|
||||
|
||||
// Act
|
||||
const filledIn = secret
|
||||
.split("\n")
|
||||
.flatMap((line) => {
|
||||
const match = /^ {2}([A-Z][A-Z0-9_]*): "(.*)"$/.exec(line);
|
||||
if (!match) return [];
|
||||
const [, key, value] = match;
|
||||
const isPlaceholder = value === "" || value.includes("CHANGE-ME");
|
||||
return isPlaceholder ? [] : [key];
|
||||
})
|
||||
.sort();
|
||||
|
||||
// Assert: die Vorlage wird committet — ein echter Wert waere damit im
|
||||
// Repository. Platzhalter sind leer oder tragen "CHANGE-ME".
|
||||
expect(filledIn, `Sieht nach echten Werten aus: ${filledIn.join(", ")}`).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user