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,109 @@
|
||||
import type { FamilienAdapter } from "@/engine/adapter/schnittstelle";
|
||||
import type { ProfilSchritt } from "@/engine/katalog/schema";
|
||||
import type { AufgeloestesProfil } from "./aufloesen";
|
||||
import { einsetzen, tiefEinsetzen, type VorlagenKontext } from "./vorlage";
|
||||
|
||||
/**
|
||||
* Baut aus aufgelösten Profilen ein Provisionierungsskript.
|
||||
*
|
||||
* Zwischen den Profilen steht eine Markenzeile (`::proxdeploy:: profil <id>`), an der die Engine
|
||||
* im Live-Log erkennt, welches Profil gerade läuft. SSH-Härtung wird ans Ende verschoben: Läuft die
|
||||
* Provisionierung selbst über SSH als root (LXC ohne Node-SSH), schlösse sie sich sonst aus.
|
||||
*/
|
||||
export const MARKE = "::proxdeploy::";
|
||||
|
||||
export type RenderKontext = {
|
||||
os: { id: string; version_id: string; codename?: string };
|
||||
gast: { name: string; benutzer: string };
|
||||
};
|
||||
|
||||
export type Abschnitt = { profil: string; skript: string };
|
||||
|
||||
export type Gerendert = { skript: string; abschnitte: Abschnitt[] };
|
||||
|
||||
function bedingungErfuellt(schritt: ProfilSchritt, werte: Record<string, unknown>): boolean {
|
||||
if (!schritt.if) return true;
|
||||
const verneint = schritt.if.startsWith("!");
|
||||
const wert = Boolean(werte[schritt.if.replace(/^!/, "")]);
|
||||
return verneint ? !wert : wert;
|
||||
}
|
||||
|
||||
function schrittRendern(schritt: ProfilSchritt, adapter: FamilienAdapter, kontext: VorlagenKontext): string {
|
||||
if ("update" in schritt) return adapter.aktualisieren();
|
||||
if ("packages" in schritt)
|
||||
return adapter.paketeInstallieren(schritt.packages.map((p) => einsetzen(p, kontext)));
|
||||
if ("service" in schritt) {
|
||||
return adapter.dienstAktivieren(einsetzen(schritt.service.name, kontext), {
|
||||
starten: schritt.service.start,
|
||||
neuStarten: schritt.service.restart,
|
||||
});
|
||||
}
|
||||
if ("repo" in schritt) {
|
||||
const r = tiefEinsetzen(schritt.repo, kontext);
|
||||
return adapter.repoHinzufuegen({
|
||||
name: r.name,
|
||||
schluesselUrl: r.key_url,
|
||||
uris: r.uris,
|
||||
suites: r.suites,
|
||||
komponenten: r.components,
|
||||
});
|
||||
}
|
||||
if ("repo_package" in schritt)
|
||||
return adapter.repoPaketInstallieren(einsetzen(schritt.repo_package, kontext));
|
||||
if ("file" in schritt) {
|
||||
const f = tiefEinsetzen(schritt.file, kontext);
|
||||
return adapter.dateiSchreiben(f.path, f.content, f.mode);
|
||||
}
|
||||
if ("timezone" in schritt) return adapter.zeitzoneSetzen(einsetzen(schritt.timezone, kontext));
|
||||
if ("locale" in schritt) return adapter.localeSetzen(einsetzen(schritt.locale, kontext));
|
||||
if ("keyboard" in schritt) return adapter.tastaturSetzen(einsetzen(schritt.keyboard, kontext));
|
||||
if ("guest_agent" in schritt) return adapter.gastAgentInstallieren();
|
||||
if ("unattended_upgrades" in schritt) return adapter.automatischeUpdates(schritt.unattended_upgrades);
|
||||
if ("run" in schritt) return einsetzen(schritt.run, kontext, "shell") + "\n";
|
||||
if ("ssh_harden" in schritt) {
|
||||
return adapter.sshHaerten({
|
||||
passwortAnmeldung: schritt.ssh_harden.password_auth,
|
||||
rootAnmeldung: schritt.ssh_harden.root_login,
|
||||
});
|
||||
}
|
||||
const unbekannt: never = schritt;
|
||||
throw new Error(`Unbekannter Schritt: ${JSON.stringify(unbekannt)}`);
|
||||
}
|
||||
|
||||
export function profileRendern(
|
||||
profile: AufgeloestesProfil[],
|
||||
adapter: FamilienAdapter,
|
||||
kontext: RenderKontext,
|
||||
vorspann: string[] = [],
|
||||
): Gerendert {
|
||||
const abschnitte: Abschnitt[] = [];
|
||||
const zumSchluss: string[] = [];
|
||||
|
||||
for (const { profil, werte } of profile) {
|
||||
const vorlagenKontext: VorlagenKontext = {
|
||||
var: werte,
|
||||
os: { id: kontext.os.id, version_id: kontext.os.version_id, codename: kontext.os.codename ?? "" },
|
||||
guest: { name: kontext.gast.name, user: kontext.gast.benutzer },
|
||||
};
|
||||
const teile: string[] = [];
|
||||
for (const schritt of profil.families[adapter.familie] ?? []) {
|
||||
if (!bedingungErfuellt(schritt, werte)) continue;
|
||||
const text = schrittRendern(schritt, adapter, vorlagenKontext);
|
||||
if ("ssh_harden" in schritt) zumSchluss.push(text);
|
||||
else teile.push(text);
|
||||
}
|
||||
abschnitte.push({ profil: profil.id, skript: teile.join("") });
|
||||
}
|
||||
|
||||
const skript = [
|
||||
"#!/bin/bash",
|
||||
"# Von ProxDeploy erzeugt",
|
||||
adapter.kopf(),
|
||||
...vorspann,
|
||||
...abschnitte.map((a) => `echo '${MARKE} profil ${a.profil}'\n${a.skript}`),
|
||||
...(zumSchluss.length > 0 ? [`echo '${MARKE} abschluss'\n${zumSchluss.join("")}`] : []),
|
||||
`echo '${MARKE} fertig'\n`,
|
||||
].join("\n");
|
||||
|
||||
return { skript, abschnitte };
|
||||
}
|
||||
Reference in New Issue
Block a user