Files
pikasTech-unidesk/scripts/src/hwlab-g14/render.ts
T
2026-06-25 16:16:25 +00:00

410 lines
16 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. render module for scripts/src/hwlab-g14.ts.
// Moved mechanically from scripts/src/hwlab-g14.ts:3346-3736 for #903.
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { createHash, randomBytes } from "node:crypto";
import { repoRoot, rootPath, type Config } from "../config";
import { runCommand } from "../command";
import { cancelJob, listJobs, readJob, startJob } from "../jobs";
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
import type { CommandJsonResult } from "./types";
import { g14HostScript } from "./images";
import { compactCommandResult, g14K3s, isCommandSuccess, record, runG14K3sRemoteAsync, shellQuote, shortSha, textHash } from "./remote";
import { runtimeLaneCicdRepoEnsureScript } from "./source";
import { CI_NAMESPACE, G14_PROVIDER, V02_CONTROL_PLANE_REFRESH_LOCK_STALE_MS, V02_CONTROL_PLANE_REFRESH_TTL_MS, V02_LANE_SPEC, V02_POLLER, V02_RECONCILER } from "./types";
import { timestampMs } from "./v02-status";
export function runtimeLaneControlPlaneRenderScript(spec: HwlabRuntimeLaneSpec, sourceCommit: string, token = runtimeLaneRenderToken()): string {
const renderDir = runtimeLaneRenderDir(spec, sourceCommit, token);
const worktreeDir = runtimeLaneRenderWorktreeDir(spec, sourceCommit, token);
return [
"set -eu",
runtimeLaneCicdRepoEnsureScript(spec),
`source_commit=${shellQuote(sourceCommit)}`,
`render_dir=${shellQuote(renderDir)}`,
`worktree_dir=${shellQuote(worktreeDir)}`,
`render_lane=${shellQuote(spec.lane)}`,
"cleanup_render_worktree() { rm -rf \"$worktree_dir\"; }",
"trap cleanup_render_worktree EXIT",
`test "$(git --git-dir="$cicd_repo" rev-parse refs/remotes/origin/${spec.sourceBranch})" = ${shellQuote(sourceCommit)}`,
"cleanup_render_worktree",
"rm -rf \"$render_dir\"",
"mkdir -p \"$render_dir\" \"$(dirname \"$worktree_dir\")\"",
"git clone --shared --no-checkout \"$cicd_repo\" \"$worktree_dir\"",
"git -C \"$worktree_dir\" checkout --detach \"$source_commit\"",
"test \"$(git -C \"$worktree_dir\" rev-parse HEAD)\" = \"$source_commit\"",
"cd \"$worktree_dir\"",
"if [ ! -d node_modules/yaml ]; then",
" if [ -f package-lock.json ]; then",
" npm ci --ignore-scripts --no-audit --prefer-offline",
" elif [ -f bun.lock ] || [ -f bun.lockb ]; then",
" bun install --frozen-lockfile --ignore-scripts",
" else",
` echo "${spec.version} control-plane render cannot install dependencies: no lockfile found" >&2`,
" exit 42",
" fi",
"fi",
"if [ -f scripts/gitops-render.mjs ]; then",
" render_script=scripts/gitops-render.mjs",
"elif [ \"$render_lane\" = v02 ] && [ -f scripts/g14-gitops-render.mjs ]; then",
" render_script=scripts/g14-gitops-render.mjs",
"else",
` echo "${spec.version} control-plane render script missing: scripts/gitops-render.mjs" >&2`,
" exit 43",
"fi",
"if [ -f scripts/run-bun.mjs ]; then",
` node scripts/run-bun.mjs "$render_script" --lane "$render_lane" --source-revision ${shellQuote(sourceCommit)} --out "$render_dir"`,
"else",
` node "$render_script" --lane "$render_lane" --source-revision ${shellQuote(sourceCommit)} --out "$render_dir"`,
"fi",
].join("\n");
}
export function v02ControlPlaneRenderScript(sourceCommit: string, token = v02RenderToken()): string {
return runtimeLaneControlPlaneRenderScript(V02_LANE_SPEC, sourceCommit, token);
}
export interface V02RenderTempResult {
result: CommandJsonResult;
renderDir: string;
worktreeDir: string;
token: string;
}
export function runRuntimeLaneRenderToTemp(spec: HwlabRuntimeLaneSpec, sourceCommit: string): V02RenderTempResult {
const token = runtimeLaneRenderToken();
return {
result: g14HostScript(runtimeLaneControlPlaneRenderScript(spec, sourceCommit, token), 180_000),
renderDir: runtimeLaneRenderDir(spec, sourceCommit, token),
worktreeDir: runtimeLaneRenderWorktreeDir(spec, sourceCommit, token),
token,
};
}
export function runV02RenderToTemp(sourceCommit: string): V02RenderTempResult {
return runRuntimeLaneRenderToTemp(V02_LANE_SPEC, sourceCommit);
}
export function runtimeLaneRenderToken(): string {
const random = Math.random().toString(16).slice(2, 10);
return `${process.pid}-${Date.now().toString(36)}-${random}`.replace(/[^a-zA-Z0-9_.-]/gu, "-");
}
export function v02RenderToken(): string {
return runtimeLaneRenderToken();
}
export function runtimeLaneRenderDir(spec: HwlabRuntimeLaneSpec, sourceCommit: string, token?: string): string {
return `/tmp/hwlab-${spec.lane}-control-plane-${shortSha(sourceCommit)}${token ? `-${token}` : ""}`;
}
export function v02RenderDir(sourceCommit: string, token?: string): string {
return runtimeLaneRenderDir(V02_LANE_SPEC, sourceCommit, token);
}
export function runtimeLaneRenderWorktreeDir(spec: HwlabRuntimeLaneSpec, sourceCommit: string, token?: string): string {
return `/tmp/hwlab-${spec.lane}-control-plane-source-${shortSha(sourceCommit)}${token ? `-${token}` : ""}`;
}
export function v02RenderWorktreeDir(sourceCommit: string, token?: string): string {
return runtimeLaneRenderWorktreeDir(V02_LANE_SPEC, sourceCommit, token);
}
export function cleanupRuntimeLaneRenderDir(spec: HwlabRuntimeLaneSpec, renderDir: string): CommandJsonResult {
return g14HostScript([
"set -eu",
`render_dir=${shellQuote(renderDir)}`,
"case \"$render_dir\" in",
` /tmp/hwlab-${spec.lane}-control-plane-*) rm -rf "$render_dir" ;;`,
` *) echo "refusing to cleanup unexpected ${spec.version} render dir: $render_dir" >&2; exit 44 ;;`,
"esac",
].join("\n"), 60_000);
}
export function cleanupV02RenderDir(renderDir: string): CommandJsonResult {
return cleanupRuntimeLaneRenderDir(V02_LANE_SPEC, renderDir);
}
export function legacyRuntimeLaneArgoApplications(spec: HwlabRuntimeLaneSpec): string[] {
if (spec.lane === "v02") return [];
return [`hwlab-g14-${spec.lane}`].filter((name) => name !== spec.app);
}
export function applyRuntimeLaneControlPlaneFiles(spec: HwlabRuntimeLaneSpec, renderDir: string, dryRun: boolean, timeoutSeconds: number): CommandJsonResult {
const namespaceFile = `${renderDir}/${spec.runtimeRenderDir}/namespace.yaml`;
const rbacFile = `${renderDir}/${spec.tektonDir}/rbac.yaml`;
const pipelineFile = `${renderDir}/${spec.tektonDir}/pipeline.yaml`;
const projectFile = `${renderDir}/argocd/project.yaml`;
const applicationFile = `${renderDir}/argocd/${spec.argoApplicationFile}`;
const resourceFiles = [namespaceFile, rbacFile, pipelineFile, projectFile, applicationFile];
const legacyApplications = legacyRuntimeLaneArgoApplications(spec);
const serverSideArgs = [
"kubectl",
"apply",
"--server-side",
"--force-conflicts",
`--field-manager=${spec.controlPlaneFieldManager}`,
...(dryRun ? ["--dry-run=server"] : []),
...resourceFiles.flatMap((file) => ["-f", file]),
];
const command = ["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, ...serverSideArgs];
const shellCommand = dryRun && spec.lane !== "v02"
? [
"set -eu",
[
...serverSideArgs.slice(0, 6),
"-f",
namespaceFile,
"-f",
pipelineFile,
"-f",
projectFile,
"-f",
applicationFile,
].map(shellQuote).join(" "),
[
"kubectl",
"apply",
"--dry-run=client",
...resourceFiles.flatMap((file) => ["-f", file]),
].map(shellQuote).join(" "),
].join("\n")
: `exec ${serverSideArgs.map(shellQuote).join(" ")}`;
const cleanupCommands = legacyApplications.map((name) => [
"kubectl",
"delete",
"application",
"-n",
"argocd",
name,
"--ignore-not-found=true",
...(dryRun ? ["--dry-run=server", "-o", "name"] : []),
].map(shellQuote).join(" "));
const script = cleanupCommands.length > 0
? ["set -eu", shellCommand.replace(/^exec /, ""), ...cleanupCommands].join("\n")
: shellCommand;
const visibleCommand = cleanupCommands.length > 0
? ["bun", "scripts/cli.ts", "ssh", `${G14_PROVIDER}:k3s`, "sh", "--", script]
: command;
return runG14K3sRemoteAsync({
script,
timeoutSeconds,
label: `${spec.lane}-control-plane-apply`,
token: runtimeLaneRenderToken(),
command: visibleCommand,
});
}
export function applyV02ControlPlaneFiles(renderDir: string, dryRun: boolean, timeoutSeconds: number): CommandJsonResult {
return applyRuntimeLaneControlPlaneFiles(V02_LANE_SPEC, renderDir, dryRun, timeoutSeconds);
}
export interface V02RefreshMarker {
ok: boolean;
sourceCommit: string;
refreshedAt: string;
renderScriptHash: string;
renderDir?: string | null;
}
export function v02ControlPlaneRefreshStateDir(): string {
return rootPath(".state", "hwlab-g14", "v02-control-plane-refresh");
}
export function v02ControlPlaneRefreshScriptHash(sourceCommit: string): string {
return textHash(v02ControlPlaneRenderScript(sourceCommit, "contract-token"));
}
export function v02ControlPlaneRefreshMarkerPath(sourceCommit: string): string {
return join(v02ControlPlaneRefreshStateDir(), `${shortSha(sourceCommit)}.json`);
}
export function v02ControlPlaneRefreshLockDir(sourceCommit: string): string {
return join(v02ControlPlaneRefreshStateDir(), `${shortSha(sourceCommit)}.lock`);
}
export function readV02RefreshMarker(sourceCommit: string, scriptHash: string, nowMs = Date.now()): V02RefreshMarker | null {
const path = v02ControlPlaneRefreshMarkerPath(sourceCommit);
if (!existsSync(path)) return null;
try {
const marker = JSON.parse(readFileSync(path, "utf8")) as V02RefreshMarker;
return v02ReusableRefreshMarker(marker, sourceCommit, scriptHash, nowMs);
} catch {
return null;
}
}
export function v02ReusableRefreshMarker(marker: unknown, sourceCommit: string, scriptHash: string, nowMs = Date.now()): V02RefreshMarker | null {
const candidate = record(marker) as V02RefreshMarker;
const refreshedAtMs = timestampMs(candidate.refreshedAt);
if (candidate.ok !== true || candidate.sourceCommit !== sourceCommit || candidate.renderScriptHash !== scriptHash) return null;
if (refreshedAtMs === null || nowMs - refreshedAtMs > V02_CONTROL_PLANE_REFRESH_TTL_MS) return null;
return candidate;
}
export function writeV02RefreshMarker(sourceCommit: string, scriptHash: string, renderDir: string | null): V02RefreshMarker {
const marker = {
ok: true,
sourceCommit,
refreshedAt: new Date().toISOString(),
renderScriptHash: scriptHash,
renderDir,
};
const dir = v02ControlPlaneRefreshStateDir();
mkdirSync(dir, { recursive: true });
writeFileSync(v02ControlPlaneRefreshMarkerPath(sourceCommit), `${JSON.stringify(marker, null, 2)}\n`, "utf8");
return marker;
}
export function acquireV02RefreshLock(sourceCommit: string): { acquired: boolean; lockDir: string; waitedMs: number } {
const stateDir = v02ControlPlaneRefreshStateDir();
mkdirSync(stateDir, { recursive: true });
const lockDir = v02ControlPlaneRefreshLockDir(sourceCommit);
const startedAtMs = Date.now();
for (;;) {
try {
mkdirSync(lockDir);
writeFileSync(join(lockDir, "owner.json"), `${JSON.stringify({ pid: process.pid, sourceCommit, acquiredAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
return { acquired: true, lockDir, waitedMs: Date.now() - startedAtMs };
} catch {
const ageMs = (() => {
try {
return Date.now() - statSync(lockDir).mtimeMs;
} catch {
return 0;
}
})();
if (ageMs > V02_CONTROL_PLANE_REFRESH_LOCK_STALE_MS) {
try {
rmSync(lockDir, { recursive: true, force: true });
continue;
} catch {
return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs };
}
}
if (Date.now() - startedAtMs >= V02_CONTROL_PLANE_REFRESH_TTL_MS) return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs };
const wait = runCommand(["sleep", "1"], repoRoot, { timeoutMs: 2_000 });
if (wait.exitCode !== 0 && wait.timedOut) return { acquired: false, lockDir, waitedMs: Date.now() - startedAtMs };
}
}
}
export function releaseV02RefreshLock(lockDir: string): void {
try {
rmSync(lockDir, { recursive: true, force: true });
} catch {
// Best-effort cleanup; stale lock expiry handles interrupted workers.
}
}
export function refreshV02ControlPlaneBeforeTrigger(sourceCommit: string, timeoutSeconds: number): Record<string, unknown> {
const scriptHash = v02ControlPlaneRefreshScriptHash(sourceCommit);
const existingMarker = readV02RefreshMarker(sourceCommit, scriptHash);
if (existingMarker !== null) {
return {
ok: true,
phase: "control-plane-refresh",
mode: "reused-recent-refresh-marker",
sourceCommit,
renderDir: existingMarker.renderDir ?? null,
marker: existingMarker,
markerTtlMs: V02_CONTROL_PLANE_REFRESH_TTL_MS,
};
}
const lock = acquireV02RefreshLock(sourceCommit);
if (!lock.acquired) {
return {
ok: false,
phase: "control-plane-refresh-lock",
mode: "local-refresh-lock",
sourceCommit,
lockDir: lock.lockDir,
waitedMs: lock.waitedMs,
degradedReason: "control-plane-refresh-lock-timeout",
};
}
try {
const markerAfterWait = readV02RefreshMarker(sourceCommit, scriptHash);
if (markerAfterWait !== null) {
return {
ok: true,
phase: "control-plane-refresh",
mode: "waited-for-recent-refresh-marker",
sourceCommit,
renderDir: markerAfterWait.renderDir ?? null,
marker: markerAfterWait,
waitedMs: lock.waitedMs,
markerTtlMs: V02_CONTROL_PLANE_REFRESH_TTL_MS,
};
}
const render = runV02RenderToTemp(sourceCommit);
if (!isCommandSuccess(render.result)) {
return {
ok: false,
phase: "source-render",
sourceCommit,
renderDir: render.renderDir,
render: compactCommandResult(render.result),
degradedReason: "control-plane-render-failed",
};
}
const apply = applyV02ControlPlaneFiles(render.renderDir, false, timeoutSeconds);
const cleanupObsoleteCronJobs = isCommandSuccess(apply) ? deleteV02ObsoleteCronJobs(false) : null;
const cleanupRenderDir = isCommandSuccess(apply) ? cleanupV02RenderDir(render.renderDir) : null;
const ok = isCommandSuccess(apply) && (cleanupObsoleteCronJobs === null || isCommandSuccess(cleanupObsoleteCronJobs));
const marker = ok ? writeV02RefreshMarker(sourceCommit, scriptHash, render.renderDir) : null;
return {
ok,
phase: "control-plane-refresh",
mode: "refreshed",
sourceCommit,
renderDir: render.renderDir,
render: compactCommandResult(render.result),
apply: compactCommandResult(apply),
cleanupObsoleteCronJobs: cleanupObsoleteCronJobs === null ? null : compactCommandResult(cleanupObsoleteCronJobs),
cleanupRenderDir: cleanupRenderDir === null ? null : compactCommandResult(cleanupRenderDir),
marker,
waitedMs: lock.waitedMs,
degradedReason: !isCommandSuccess(apply)
? "control-plane-apply-failed"
: cleanupObsoleteCronJobs !== null && !isCommandSuccess(cleanupObsoleteCronJobs)
? "obsolete-cronjob-cleanup-failed"
: undefined,
};
} finally {
releaseV02RefreshLock(lock.lockDir);
}
}
export function getV02ObsoleteCronJobs(): CommandJsonResult {
return g14K3s([
"kubectl",
"get",
"cronjob",
"-n",
CI_NAMESPACE,
V02_POLLER,
V02_RECONCILER,
"--ignore-not-found",
"-o",
"name",
], 60_000);
}
export function deleteV02ObsoleteCronJobs(dryRun: boolean): CommandJsonResult {
return g14K3s([
"kubectl",
"delete",
"cronjob",
"-n",
CI_NAMESPACE,
V02_POLLER,
V02_RECONCILER,
"--ignore-not-found=true",
...(dryRun ? ["--dry-run=server", "-o", "name"] : []),
], 60_000);
}