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:
@@ -25,7 +25,7 @@ export const NAVIGATION: readonly NavigationsEintrag[] = [
|
||||
{ pfad: "/ausrollen", titel: "Ausrollen", symbol: "ausrollen", recht: "ausrollen" },
|
||||
{ pfad: "/auftraege", titel: "Verlauf", symbol: "verlauf", recht: "lesen" },
|
||||
{ pfad: "/templates", titel: "Templates", symbol: "templates", recht: "lesen" },
|
||||
{ pfad: "/vorlagen", titel: "Vorlagen", symbol: "vorlagen", recht: "lesen", ab: "#012" },
|
||||
{ pfad: "/vorlagen", titel: "Vorlagen", symbol: "vorlagen", recht: "lesen" },
|
||||
{ pfad: "/katalog", titel: "Katalog", symbol: "katalog", recht: "lesen" },
|
||||
{ pfad: "/server", titel: "Server", symbol: "server", recht: "lesen" },
|
||||
{
|
||||
|
||||
@@ -105,6 +105,8 @@ export const AusrollAnfrage = z
|
||||
/** Fehlt das Template, vorher bauen (sonst bricht die Prüfung mit Hinweis ab). */
|
||||
templateBauen: z.boolean().default(false),
|
||||
startBeiBoot: z.boolean().default(true),
|
||||
/** Name der Vorlage, die angewendet wurde (nur zur Nachverfolgung; angewendet wird vorher). */
|
||||
vorlage: z.string().optional(),
|
||||
})
|
||||
.refine((a) => a.typ === "lxc" || a.diskGb >= 4, {
|
||||
message: "Mindestens 4 GB für eine VM.",
|
||||
@@ -120,6 +122,7 @@ export type AusrollEingabe = z.input<typeof AusrollAnfrage>;
|
||||
*/
|
||||
export function cliBefehl(a: AusrollAnfrage): string {
|
||||
const teile = [`proxdeploy ${a.typ} ${a.name}`, `--server ${a.server}`];
|
||||
if (a.vorlage) teile.push(`--preset ${a.vorlage}`);
|
||||
if (a.knoten) teile.push(`--node ${a.knoten}`);
|
||||
teile.push(`--distro ${a.distribution}`);
|
||||
if (a.profile.length > 0 && !(a.profile.length === 1 && a.profile[0] === "base"))
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { z } from "@/lib/validierung";
|
||||
|
||||
/**
|
||||
* Was eine Vorlage festlegen darf. Bewusst ohne Name, VMID, IP und Schlüssel – das ist je Gast
|
||||
* verschieden. Server und Storage dürfen drin stehen, sind aber optional.
|
||||
*/
|
||||
export const VorlagenParameter = z
|
||||
.object({
|
||||
typ: z.enum(["vm", "lxc"]).optional(),
|
||||
server: z.string().trim().optional(),
|
||||
distribution: z.string().trim().optional(),
|
||||
cores: z.coerce.number().int().min(1).max(128).optional(),
|
||||
speicherMb: z.coerce.number().int().min(256).optional(),
|
||||
diskGb: z.coerce.number().int().min(1).optional(),
|
||||
storage: z.string().trim().optional(),
|
||||
swapMb: z.coerce.number().int().min(0).optional(),
|
||||
unprivilegiert: z.boolean().optional(),
|
||||
features: z.object({ nesting: z.boolean(), keyctl: z.boolean() }).partial().optional(),
|
||||
netz: z
|
||||
.object({ bridge: z.string().trim(), vlan: z.coerce.number().int().min(1).max(4094) })
|
||||
.partial()
|
||||
.optional(),
|
||||
profile: z.array(z.string()).optional(),
|
||||
variablen: z
|
||||
.record(z.string(), z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])))
|
||||
.optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type VorlagenParameter = z.infer<typeof VorlagenParameter>;
|
||||
|
||||
export const VorlageEingabe = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.regex(/^[a-z0-9][a-z0-9-]{0,47}$/, "Kurzname: Kleinbuchstaben, Ziffern und Bindestrich."),
|
||||
titel: z.string().trim().min(1, "Titel fehlt.").max(80),
|
||||
beschreibung: z.string().trim().max(500).optional(),
|
||||
parameter: VorlagenParameter,
|
||||
});
|
||||
|
||||
/**
|
||||
* Legt eine Vorlage unter eine Anfrage: was die Anfrage selbst angibt, gewinnt. Objekte (netz,
|
||||
* features, variablen) werden feldweise gemischt, Listen ersetzt.
|
||||
*/
|
||||
export function vorlageAnwenden(
|
||||
vorlage: VorlagenParameter,
|
||||
anfrage: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const ergebnis: Record<string, unknown> = { ...vorlage };
|
||||
for (const [schluessel, wert] of Object.entries(anfrage)) {
|
||||
if (wert === undefined) continue;
|
||||
const basis = ergebnis[schluessel];
|
||||
if (
|
||||
basis &&
|
||||
typeof basis === "object" &&
|
||||
!Array.isArray(basis) &&
|
||||
wert &&
|
||||
typeof wert === "object" &&
|
||||
!Array.isArray(wert)
|
||||
) {
|
||||
ergebnis[schluessel] =
|
||||
schluessel === "variablen"
|
||||
? Object.fromEntries(
|
||||
[...new Set([...Object.keys(basis), ...Object.keys(wert)])].map((p) => [
|
||||
p,
|
||||
{ ...(basis as Record<string, object>)[p], ...(wert as Record<string, object>)[p] },
|
||||
]),
|
||||
)
|
||||
: { ...basis, ...wert };
|
||||
} else {
|
||||
ergebnis[schluessel] = wert;
|
||||
}
|
||||
}
|
||||
return ergebnis;
|
||||
}
|
||||
Reference in New Issue
Block a user