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:
2026-10-12 10:53:38 +02:00
co-authored by Claude Opus 5.5
parent acae1bc2a4
commit 01d22aac3a
15 changed files with 999 additions and 10 deletions
+55
View File
@@ -0,0 +1,55 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { debianAdapter } from "@/engine/adapter/debian";
import { katalogLaden } from "@/engine/katalog/laden";
import { profileAufloesen } from "@/engine/profile/aufloesen";
import { profileRendern } from "@/engine/profile/rendern";
/** shellcheck über Docker – läuft nur, wenn Docker und das Image da sind, und sagt das sonst. */
function shellcheckVerfuegbar(): boolean {
try {
execFileSync("docker", ["image", "inspect", "koalaman/shellcheck:stable"], { stdio: "ignore" });
return true;
} catch {
console.warn("shellcheck übersprungen: docker oder koalaman/shellcheck:stable fehlt");
return false;
}
}
describe.runIf(shellcheckVerfuegbar())("shellcheck", () => {
it("findet in den gerenderten Katalogprofilen nichts", async () => {
const katalog = await katalogLaden("catalog");
const profile = new Map(katalog.profile.map((p) => [p.eintrag.id, p.eintrag]));
const ordner = mkdtempSync(join(tmpdir(), "pd-sc-"));
for (const { eintrag: distro } of katalog.distributionen) {
const aufgeloest = profileAufloesen(["docker", "zabbix-agent"], profile, "debian", {
base: { fail2ban: true },
"zabbix-agent": { server: "zbx" },
});
if (!aufgeloest.ok) throw new Error("Auflösung fehlgeschlagen");
const { skript } = profileRendern(
aufgeloest.profile,
debianAdapter,
{ os: distro.os, gast: { name: "g", benutzer: "u" } },
[debianAdapter.benutzerAnlegen({ name: "u", sshSchluessel: ["ssh-ed25519 AAAA u"], sudo: true })],
);
writeFileSync(join(ordner, `${distro.id}.sh`), skript);
}
const ausgabe = (() => {
try {
execFileSync(
"bash",
["-c", `docker run --rm -v ${ordner}:/s:ro koalaman/shellcheck:stable -s bash /s/*.sh`],
{ stdio: "pipe" },
);
return "";
} catch (fehler) {
return String((fehler as { stdout?: Buffer }).stdout ?? fehler);
}
})();
expect(ausgabe).toBe("");
}, 60_000);
});