feat(vorlagen): Vorlagen (Presets) für Assistent, API und CLI
Gespeicherte Kombinationen aus Distribution, Ressourcen und Profilen. Im Assistenten mit einem Klick vorbelegt oder dort gespeichert; die API nimmt „vorlage“ entgegen (Grundlage für --preset der CLI). Eine Vorlage liegt unter der Anfrage – was die Anfrage selbst sagt, gewinnt. Beim ersten Start wird das Beispiel „Docker-Host klein“ aus der Vorgabe angelegt, einmalig. Closes #012 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { anmelden, E2E_PRAEFIX, keinQuerscrollen } from "./helfer";
|
||||
|
||||
/**
|
||||
* Braucht einen Server mit Verbindungstest (der Assistent zeigt sonst nur einen Hinweis). Gibt es
|
||||
* keinen, wird der Ablauf übersprungen und sagt das.
|
||||
*/
|
||||
test("Vorlage im Assistenten anwenden und als neue Vorlage speichern", async ({ page }) => {
|
||||
const name = `${E2E_PRAEFIX}-${test.info().project.name}`;
|
||||
await anmelden(page, "/vorlagen");
|
||||
await expect(page.getByText("Docker-Host klein")).toBeVisible();
|
||||
await keinQuerscrollen(page);
|
||||
await page.getByRole("link", { name: "Ausrollen mit dieser Vorlage" }).first().click();
|
||||
if (await page.getByText("Erst einen Server anlegen").isVisible())
|
||||
test.skip(true, "Kein getesteter Server vorhanden");
|
||||
await expect(page.getByLabel("Vorlage")).toHaveValue("docker-host-klein");
|
||||
|
||||
// Vorbelegung: Ressourcen aus der Vorlage
|
||||
for (let i = 0; i < 3; i++) await page.getByRole("button", { name: "Weiter" }).click();
|
||||
await page.getByLabel("Name (Hostname)").fill("pd-vorlage");
|
||||
await page.getByRole("button", { name: "Weiter" }).click();
|
||||
await expect(page.getByLabel("RAM (MB)")).toHaveValue("4096");
|
||||
await expect(page.getByLabel("Disk (GB)")).toHaveValue("40");
|
||||
|
||||
// Zur Zusammenfassung springen geht nicht ohne Schlüssel – Schritt 7 ausfüllen.
|
||||
await page.getByRole("button", { name: /07 Zugang/ }).click();
|
||||
await page
|
||||
.getByLabel("Öffentliche SSH-Schlüssel")
|
||||
.fill("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEFTdkCuqQ6mF7I8QzmkNaQpKj7S/I2+yuxWeYasJjLd e2e@test");
|
||||
await page.getByRole("button", { name: "Weiter" }).click();
|
||||
await expect(page.getByLabel("Docker CE")).toBeChecked();
|
||||
await page.getByRole("button", { name: "Weiter" }).click();
|
||||
await expect(page.getByText(/--preset docker-host-klein/)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
await page.getByRole("button", { name: "Als Vorlage speichern" }).click();
|
||||
await page.getByLabel("Kurzname (für --preset)").fill(name);
|
||||
await page.getByLabel("Titel").fill(`E2E ${test.info().project.name}`);
|
||||
await page.getByRole("button", { name: "Speichern" }).click();
|
||||
await expect(page.getByText(/gespeichert/)).toBeVisible();
|
||||
|
||||
await page.goto("/vorlagen");
|
||||
await expect(page.getByText(`--preset ${name}`)).toBeVisible();
|
||||
page.once("dialog", (d) => void d.accept());
|
||||
await page
|
||||
.locator("li", { hasText: `--preset ${name}` })
|
||||
.getByRole("button", { name: "Löschen" })
|
||||
.click();
|
||||
await expect(page.getByText(`--preset ${name}`)).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import * as liste from "@/app/api/v1/vorlagen/route";
|
||||
import * as einzeln from "@/app/api/v1/vorlagen/[name]/route";
|
||||
import { verschluesseln } from "@/lib/geheimnisse";
|
||||
import { vorlageAnwenden } from "@/lib/schemas/vorlage";
|
||||
import { ausrollenVorbereiten } from "@/server/services/ausrollen";
|
||||
import { katalogSynchronisieren } from "@/server/services/katalog";
|
||||
import { beispielVorlageAnlegen } from "@/server/services/vorlagen";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
import { angemeldet, anfrage, kontext } from "./helpers/anfragen";
|
||||
|
||||
const SCHLUESSEL =
|
||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEFTdkCuqQ6mF7I8QzmkNaQpKj7S/I2+yuxWeYasJjLd florian@test";
|
||||
|
||||
describe("Vorlage anwenden", () => {
|
||||
it("Anfrage gewinnt; netz und variablen werden feldweise gemischt, Listen ersetzt", () => {
|
||||
const ergebnis = vorlageAnwenden(
|
||||
{
|
||||
cores: 2,
|
||||
speicherMb: 4096,
|
||||
profile: ["base", "docker"],
|
||||
netz: { bridge: "vmbr1", vlan: 20 },
|
||||
variablen: { "zabbix-agent": { server: "zbx" } },
|
||||
},
|
||||
{
|
||||
cores: 4,
|
||||
netz: { modus: "dhcp" },
|
||||
profile: ["base"],
|
||||
variablen: { "zabbix-agent": { server_active: "zbx2" } },
|
||||
name: undefined,
|
||||
},
|
||||
);
|
||||
expect(ergebnis).toEqual({
|
||||
cores: 4,
|
||||
speicherMb: 4096,
|
||||
profile: ["base"],
|
||||
netz: { bridge: "vmbr1", vlan: 20, modus: "dhcp" },
|
||||
variablen: { "zabbix-agent": { server: "zbx", server_active: "zbx2" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Vorlagen", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
await katalogSynchronisieren();
|
||||
});
|
||||
|
||||
it("Beispiel „Docker-Host klein“ wird einmal angelegt und kommt nach dem Löschen nicht wieder", async () => {
|
||||
await beispielVorlageAnlegen();
|
||||
expect(
|
||||
(await testPrisma.vorlage.findUniqueOrThrow({ where: { name: "docker-host-klein" } })).parameter,
|
||||
).toMatchObject({
|
||||
distribution: "debian-13",
|
||||
cores: 2,
|
||||
speicherMb: 4096,
|
||||
profile: ["base", "docker"],
|
||||
});
|
||||
await testPrisma.vorlage.delete({ where: { name: "docker-host-klein" } });
|
||||
await beispielVorlageAnlegen();
|
||||
expect(await testPrisma.vorlage.count()).toBe(0);
|
||||
});
|
||||
|
||||
it("CRUD über die API; Operator darf pflegen; Name, IP und Schlüssel gehören nicht hinein", async () => {
|
||||
const operator = (await angemeldet("OPERATOR")).cookie;
|
||||
const neu = {
|
||||
name: "Web-Klein",
|
||||
titel: "Web klein",
|
||||
parameter: { typ: "vm", distribution: "debian-13", cores: 1 },
|
||||
};
|
||||
const angelegt = await liste.POST(anfrage("POST", "/api/v1/vorlagen", operator, neu));
|
||||
expect(angelegt.status).toBe(201);
|
||||
expect((await angelegt.json()).name).toBe("web-klein");
|
||||
expect((await liste.POST(anfrage("POST", "/api/v1/vorlagen", operator, neu))).status).toBe(409);
|
||||
const mitName = await liste.POST(
|
||||
anfrage("POST", "/api/v1/vorlagen", operator, { ...neu, name: "x", parameter: { name: "web01" } }),
|
||||
);
|
||||
expect(mitName.status).toBe(400);
|
||||
const geaendert = await einzeln.PUT(
|
||||
anfrage("PUT", "/api/v1/vorlagen/web-klein", operator, {
|
||||
...neu,
|
||||
titel: "Web, klein",
|
||||
parameter: { cores: 2 },
|
||||
}),
|
||||
kontext({ name: "web-klein" }),
|
||||
);
|
||||
expect((await geaendert.json()).parameter).toEqual({ cores: 2 });
|
||||
expect(
|
||||
(
|
||||
await einzeln.DELETE(
|
||||
anfrage("DELETE", "/api/v1/vorlagen/web-klein", operator),
|
||||
kontext({ name: "web-klein" }),
|
||||
)
|
||||
).status,
|
||||
).toBe(204);
|
||||
expect(await testPrisma.protokoll.count({ where: { ziel: "vorlage:web-klein" } })).toBe(3);
|
||||
});
|
||||
|
||||
it("Ausrollen mit vorlage: Werte der Vorlage, Anfrage gewinnt, Name im CLI-Befehl", async () => {
|
||||
await beispielVorlageAnlegen();
|
||||
await testPrisma.server.create({
|
||||
data: {
|
||||
name: "rz1",
|
||||
titel: "RZ 1",
|
||||
url: "https://127.0.0.1:8006",
|
||||
tokenId: "a@pve!b",
|
||||
tokenGeheimnis: verschluesseln("x"),
|
||||
fingerabdruck: "AA",
|
||||
},
|
||||
});
|
||||
const v = await ausrollenVorbereiten({
|
||||
vorlage: "docker-host-klein",
|
||||
server: "rz1",
|
||||
name: "web01",
|
||||
storage: "local-zfs",
|
||||
speicherMb: 8192,
|
||||
netz: { bridge: "vmbr0" },
|
||||
zugang: { benutzer: "florian", sshSchluessel: [SCHLUESSEL] },
|
||||
});
|
||||
expect(v.anfrage).toMatchObject({
|
||||
typ: "vm",
|
||||
distribution: "debian-13",
|
||||
cores: 2,
|
||||
speicherMb: 8192,
|
||||
diskGb: 40,
|
||||
vorlage: "docker-host-klein",
|
||||
});
|
||||
expect(v.profile.map((p) => p.profil.id)).toEqual(["base", "docker"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user