feat(engine): Adapter debian und Profil-Engine
Profile sind deklarative Schritte je Distributionsfamilie; der Adapter übersetzt sie in ein idempotentes Shell-Skript. Ein neuer Adapter (rhel, alpine) ist eine Datei und ein Eintrag in der Registry. Variablen werden typisiert geprüft, in run-Schritten jeder Wert für die Shell gequotet. SSH-Härtung läuft zuletzt, damit sich eine Provisionierung per SSH nicht selbst aussperrt. Geprüft mit shellcheck und echt in Containern (Debian 12/13, Ubuntu 22.04/24.04/26.04) inklusive zweitem Lauf. Dabei aufgefallen: frische Images haben keine Paketlisten – der Vorspann macht jetzt apt-get update. Closes #005 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import type { Profil, ProfilVariable } from "@/engine/katalog/schema";
|
||||
|
||||
/**
|
||||
* Aus der Auswahl im Assistenten wird eine geordnete Liste: Voraussetzungen zuerst (`requires`,
|
||||
* transitiv), sonst die Reihenfolge der Auswahl. Dazu die Variablen mit Standardwerten und
|
||||
* Typprüfung. Fehler werden gesammelt, damit das Formular alle auf einmal zeigen kann.
|
||||
*/
|
||||
export type AufgeloestesProfil = { profil: Profil; werte: Record<string, string | number | boolean> };
|
||||
|
||||
export type ProfilFehler = { profil: string; variable?: string; meldung: string };
|
||||
|
||||
export type Aufloesung =
|
||||
{ ok: true; profile: AufgeloestesProfil[]; automatisch: string[] } | { ok: false; fehler: ProfilFehler[] };
|
||||
|
||||
export function profileAufloesen(
|
||||
auswahl: string[],
|
||||
verfuegbar: ReadonlyMap<string, Profil>,
|
||||
familie: string,
|
||||
eingaben: Record<string, Record<string, unknown>> = {},
|
||||
): Aufloesung {
|
||||
const fehler: ProfilFehler[] = [];
|
||||
const reihenfolge: string[] = [];
|
||||
const besucht = new Set<string>();
|
||||
const inArbeit = new Set<string>();
|
||||
|
||||
const besuchen = (id: string, von: string | null) => {
|
||||
if (besucht.has(id)) return;
|
||||
if (inArbeit.has(id)) {
|
||||
fehler.push({ profil: id, meldung: `Zyklische Voraussetzung über „${von}“.` });
|
||||
return;
|
||||
}
|
||||
const profil = verfuegbar.get(id);
|
||||
if (!profil) {
|
||||
fehler.push({
|
||||
profil: id,
|
||||
meldung: von
|
||||
? `Wird von „${von}“ vorausgesetzt, ist aber nicht verfügbar.`
|
||||
: "Unbekanntes oder deaktiviertes Profil.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
inArbeit.add(id);
|
||||
for (const voraussetzung of profil.requires) besuchen(voraussetzung, id);
|
||||
inArbeit.delete(id);
|
||||
besucht.add(id);
|
||||
reihenfolge.push(id);
|
||||
};
|
||||
for (const id of auswahl) besuchen(id, null);
|
||||
|
||||
const profile: AufgeloestesProfil[] = [];
|
||||
for (const id of reihenfolge) {
|
||||
const profil = verfuegbar.get(id)!;
|
||||
if (!profil.families[familie]) {
|
||||
fehler.push({ profil: id, meldung: `Keine Umsetzung für die Familie „${familie}“.` });
|
||||
continue;
|
||||
}
|
||||
const werte: Record<string, string | number | boolean> = {};
|
||||
for (const variable of profil.variables) {
|
||||
const ergebnis = wertPruefen(variable, eingaben[id]?.[variable.name]);
|
||||
if ("fehler" in ergebnis)
|
||||
fehler.push({ profil: id, variable: variable.name, meldung: ergebnis.fehler });
|
||||
else werte[variable.name] = ergebnis.wert;
|
||||
}
|
||||
profile.push({ profil, werte });
|
||||
}
|
||||
|
||||
if (fehler.length > 0) return { ok: false, fehler };
|
||||
return { ok: true, profile, automatisch: reihenfolge.filter((id) => !auswahl.includes(id)) };
|
||||
}
|
||||
|
||||
function wertPruefen(
|
||||
variable: ProfilVariable,
|
||||
eingabe: unknown,
|
||||
): { wert: string | number | boolean } | { fehler: string } {
|
||||
const roh = eingabe === undefined || eingabe === null || eingabe === "" ? variable.default : eingabe;
|
||||
if (roh === undefined || roh === "") {
|
||||
if (variable.required) return { fehler: `${variable.label} fehlt.` };
|
||||
return { wert: variable.type === "boolean" ? false : "" };
|
||||
}
|
||||
switch (variable.type) {
|
||||
case "boolean":
|
||||
if (typeof roh === "boolean") return { wert: roh };
|
||||
if (roh === "true" || roh === "1") return { wert: true };
|
||||
if (roh === "false" || roh === "0") return { wert: false };
|
||||
return { fehler: `${variable.label}: ja oder nein erwartet.` };
|
||||
case "number": {
|
||||
const zahl = typeof roh === "number" ? roh : Number(roh);
|
||||
return Number.isFinite(zahl) ? { wert: zahl } : { fehler: `${variable.label}: Zahl erwartet.` };
|
||||
}
|
||||
case "select":
|
||||
return variable.options!.includes(String(roh))
|
||||
? { wert: String(roh) }
|
||||
: { fehler: `${variable.label}: erlaubt sind ${variable.options!.join(", ")}.` };
|
||||
default: {
|
||||
const text = String(roh);
|
||||
// Ein Zeilenumbruch in einer Variable ist fast immer ein Kopierfehler – und in Konfigurationsdateien gefährlich.
|
||||
if (/[\r\n\0]/.test(text)) return { fehler: `${variable.label}: keine Zeilenumbrüche.` };
|
||||
return { wert: text };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user