Files
proxdeploy/src/engine/profile/aufloesen.ts
T
florian.wildeandClaude Opus 5.5 00712dc6ce feat(templates): Profile ins Template einbacken
Beim Template-Bau lassen sich Profile wählen (nur solche ohne
Pflichtvariablen). Nach dem Vorbereitungsstart laufen sie über den
Guest-Agent; danach werden Maschinen-ID, SSH-Host-Keys, Docker-Engine-ID
und apt-Cache entfernt. Ausrollen nimmt das Template mit den meisten
passenden Profilen und überspringt, was schon eingebacken ist.

Auf pve-demo: Provisionierung mit base+docker 141 s ohne, ~30 s mit
Template. Dabei gefunden: docker nimmt den Benutzer per guest.user in
die Gruppe auf – Profile mit guest.* laufen deshalb beim Deploy noch
einmal (idempotent, schnell).

Closes #022

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:20:32 +02:00

136 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)) };
}
/** Werte eines Profils aus der Eingabe, mit Standardwerten und Typprüfung. */
export function variablenWerte(
profil: Profil,
eingabe: Record<string, unknown>,
): { werte: Record<string, string | number | boolean>; fehler: ProfilFehler[] } {
const werte: Record<string, string | number | boolean> = {};
const fehler: ProfilFehler[] = [];
for (const variable of profil.variables) {
const ergebnis = wertPruefen(variable, eingabe[variable.name]);
if ("fehler" in ergebnis)
fehler.push({ profil: profil.id, variable: variable.name, meldung: ergebnis.fehler });
else werte[variable.name] = ergebnis.wert;
}
return { werte, fehler };
}
/**
* Ein ins Template eingebackenes Profil muss beim Deploy nur dann nicht noch einmal laufen, wenn
* seine Variablen genau die Standardwerte haben – mit denen wurde es eingebacken.
*/
export function mitStandardwerten(eintrag: AufgeloestesProfil): boolean {
const standard = variablenWerte(eintrag.profil, {}).werte;
return JSON.stringify(standard) === JSON.stringify(eintrag.werte);
}
/**
* Verweist ein Profil auf `{{ guest.* }}` (Name, Benutzer), hängt es vom einzelnen Gast ab – im
* Template stand dort etwas anderes. Solche Profile laufen beim Deploy trotzdem noch einmal; dank
* Idempotenz ist das schnell (Fund: docker nahm sonst den Benutzer nicht in die Gruppe auf).
*/
export function gastunabhaengig(profil: Profil): boolean {
return !/\{\{[^}]*\bguest\./.test(JSON.stringify(profil.families));
}
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 };
}
}
}