e6f4ae470b
- templates custom/bootstrap-root + identity-network (sortie structurée parsable, sauvegardes, échec contrôlé, jamais de coupure réseau sans reconnexion) - postInstall: registre de manifestes (champs typés + defaults/defaultFrom), validateProfileValues + maskSecretValues + buildPostInstallResult (TDD), renderProfile/previewProfile (masquage secrets), runPostInstall (SSH) - execute: RunActionOpts.profileId/values + branche post_install (bloc postInstall) - action_requests: post_install accepté, payload profileId/values transmis à approve - routes: GET /profiles, POST .../preview (script masqué + validation), POST .../run (action_request si requiresConfirmation, sinon direct) Champs = formulaire (pas de question SSH interactive) ; secrets jamais sérialisés ; identity_network exige confirmation. tsc 0 · 101 tests · build OK · boot OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
// server/templates/render.ts
|
|
import Mustache from "mustache";
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
const TEMPLATES_ROOT = resolve(process.cwd(), "templates");
|
|
|
|
export interface TemplateVars {
|
|
aptProxy?: string | null;
|
|
aptProxyUrl?: string | null; // proxy persistant (apt_proxy_persistent)
|
|
// Docker template vars
|
|
composeRoots?: string | number | null;
|
|
composeScanDepth?: string | number | null;
|
|
stackDir?: string | null;
|
|
// Post-install (SJ-8) — toutes optionnelles, jamais de secret.
|
|
operatorUser?: string | null;
|
|
packages?: string | null; // liste shell-safe rendue par le backend
|
|
newHostname?: string | null;
|
|
domain?: string | null;
|
|
interfaceName?: string | null;
|
|
staticAddress?: string | null;
|
|
gateway?: string | null;
|
|
dnsNameservers?: string | null;
|
|
reconnectHost?: string | null;
|
|
dhcpEndpoint?: string | null;
|
|
dockerUser?: string | null;
|
|
composeRoot?: string | null;
|
|
rebootAfterInstall?: boolean;
|
|
[key: string]: unknown; // champs de profil dynamiques (typés au cas par cas)
|
|
}
|
|
|
|
export function renderTemplate(
|
|
relPath: string,
|
|
vars: TemplateVars,
|
|
opts?: { tags?: [string, string] },
|
|
): string {
|
|
const tpl = readFileSync(resolve(TEMPLATES_ROOT, relPath), "utf8");
|
|
// Les templates Docker contiennent des Go-templates {{...}} : on bascule les
|
|
// délimiteurs Mustache sur <% %> pour ne pas les interpréter.
|
|
const tags = opts?.tags ?? (relPath.startsWith("docker/") ? (["<%", "%>"] as [string, string]) : undefined);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return Mustache.render(tpl, vars, {}, { escape: (s: any) => s, ...(tags ? { tags } : {}) } as any);
|
|
}
|
|
|
|
/** Existence par défaut d'un template relatif à templates/. */
|
|
function defaultExists(rel: string): boolean {
|
|
return existsSync(resolve(TEMPLATES_ROOT, rel));
|
|
}
|
|
|
|
/**
|
|
* Résout le chemin de template le plus spécifique pour (action, OS) :
|
|
* `<osFamily>/<action>.sh.tpl` s'il existe, sinon fallback base `apt/<action>.sh.tpl`.
|
|
* `exists` est injectable pour les tests.
|
|
*/
|
|
export function resolveTemplate(
|
|
action: string,
|
|
osFamily: string,
|
|
exists: (rel: string) => boolean = defaultExists,
|
|
): string {
|
|
const specific = `${osFamily}/${action}.sh.tpl`;
|
|
if (osFamily !== "unknown" && osFamily !== "apt" && exists(specific)) return specific;
|
|
return `apt/${action}.sh.tpl`;
|
|
}
|