fix: 收敛 HWLAB runtime GitOps ConfigMap ownership

This commit is contained in:
Codex
2026-07-13 23:34:40 +02:00
parent 9833f5fd6b
commit 75e04b49e5
6 changed files with 128 additions and 27 deletions
@@ -2,6 +2,8 @@
import { spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { runtimeGitopsScriptsConfigMap } from "../hwlab/runtime-gitops-scripts-configmap.mjs";
import { hwlabRuntimeLaneSpecForNode } from "../../src/hwlab-node-lanes.ts";
function option(name) {
const index = process.argv.indexOf(name);
@@ -43,6 +45,47 @@ function safeReleaseStatePath(value) {
return path;
}
function safeResourceManifestPath(value, pathLabel) {
const path = required(value, pathLabel);
if (path.startsWith("/") || path.split("/").includes("..") || !path.endsWith(".yaml")) throw new Error(`${pathLabel} must be a safe relative .yaml path`);
return path;
}
export function renderGitOpsResources(gitops, sourceRoot) {
if (gitops.resources === undefined) return [];
if (!Array.isArray(gitops.resources)) throw new Error("delivery.gitops.resources must be an array");
return gitops.resources.map((value, index) => {
const pathLabel = `delivery.gitops.resources[${index}]`;
const resource = record(value, pathLabel);
const renderer = required(resource.renderer, `${pathLabel}.renderer`);
if (renderer !== "hwlab-runtime-gitops-scripts") throw new Error(`${pathLabel}.renderer is unsupported: ${renderer}`);
const configRef = required(resource.configRef, `${pathLabel}.configRef`);
const match = /^config\/hwlab-node-lanes\.yaml#lanes\.([^.]+)\.targets\.([^.]+)$/u.exec(configRef);
if (match === null) throw new Error(`${pathLabel}.configRef must select config/hwlab-node-lanes.yaml#lanes.<lane>.targets.<node>`);
const lane = match[1];
const node = match[2];
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
const overlay = {
nodeId: spec.nodeId,
lane: spec.lane,
runtimePath: spec.runtimePath,
observability: spec.observability,
};
const configMapName = `${spec.pipeline}-runtime-gitops-scripts`;
const manifest = Bun.YAML.stringify(runtimeGitopsScriptsConfigMap({
overlay,
scriptsDir: resolve(sourceRoot, "scripts/native/hwlab"),
namespace: required(resource.namespace, `${pathLabel}.namespace`),
configMapName,
}));
return {
id: required(resource.id, `${pathLabel}.id`),
path: safeResourceManifestPath(resource.manifestPath, `${pathLabel}.manifestPath`),
content: `${manifest.trimEnd()}\n`,
};
});
}
function releaseValue(releaseDir, name) {
return required(readFileSync(resolve(releaseDir, name), "utf8").trim(), `${releaseDir}/${name}`);
}
@@ -100,6 +143,7 @@ const writeUrl = required(gitops.writeUrl, "delivery.gitops.writeUrl");
const branch = required(gitops.branch, "delivery.gitops.branch");
const manifestPath = safeManifestPath(gitops.manifestPath);
const releaseStatePath = safeReleaseStatePath(gitops.releaseStatePath);
const gitopsResources = renderGitOpsResources(gitops, sourceRoot);
let digest = null;
let digestRef = null;
let manifest = null;
@@ -163,12 +207,23 @@ if (action === "skip") {
removeFile(statePath);
}
for (const resource of gitopsResources) {
const resourcePath = resolve(worktree, resource.path);
if (!resourcePath.startsWith(`${worktree}/`)) throw new Error(`resolved resource path escaped the GitOps worktree: ${resource.id}`);
if (enabled) {
mkdirSync(dirname(resourcePath), { recursive: true });
writeFileSync(resourcePath, resource.content, "utf8");
} else {
removeFile(resourcePath);
}
}
run("git", ["config", "user.name", required(author.name, "delivery.gitops.author.name")], worktree);
run("git", ["config", "user.email", required(author.email, "delivery.gitops.author.email")], worktree);
run("git", ["add", "-A"], worktree);
const changed = run("git", ["diff", "--cached", "--quiet"], worktree, true).status !== 0;
if (changed) {
const commitAction = action === "build" ? "deploy" : "remove";
const commitAction = action === "disabled" ? "remove" : "deploy";
run("git", ["commit", "-m", `unidesk-host: ${commitAction} ${serviceRef} ${sourceCommit.slice(0, 12)}`], worktree);
}
@@ -208,10 +263,11 @@ process.stdout.write(`${JSON.stringify({
digest,
digestRef,
gitopsCommit,
resources: gitopsResources.map((resource) => ({ id: resource.id, path: resource.path })),
changed,
pushAttempts,
valuesPrinted: false,
})}\n`);
}
if (!process.execArgv.includes("--check")) main();
if (import.meta.main && !process.execArgv.includes("--check")) main();
@@ -4,6 +4,7 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import { runtimeGitopsScriptsConfigMap } from "./runtime-gitops-scripts-configmap.mjs";
const requireFromScript = createRequire(import.meta.url);
const requireFromCwd = createRequire(path.join(process.cwd(), "package.json"));
@@ -125,31 +126,9 @@ function ensureRuntimeGitopsScriptsMount(taskSpec, step) {
function writeScriptsConfigMap(targetPath, namespace) {
if (scriptsDir === null) throw new Error("--scripts-dir is required with --scripts-configmap");
const data = {
"runtime-gitops-overlay.json": `${JSON.stringify({
runtimePath: requiredOverlayString("runtimePath"),
observability: objectOr(overlay.observability),
})}\n`,
};
for (const name of ["runtime-gitops-observability.mjs", "runtime-gitops-postprocess.mjs", "runtime-gitops-verify.mjs"]) {
data[name] = readFileSync(path.join(scriptsDir, name), "utf8");
}
writeFileSync(targetPath, `${YAML.stringify({
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: configMapName,
namespace,
labels: {
"app.kubernetes.io/name": "hwlab-runtime-gitops-scripts",
"app.kubernetes.io/part-of": "unidesk-hwlab-control-plane",
"hwlab.pikastech.local/node": overlay.nodeId ?? null,
"hwlab.pikastech.local/lane": overlay.lane ?? null,
},
},
data,
}).trimEnd()}\n`, "utf8");
return { name: configMapName, namespace, keyCount: Object.keys(data).length, path: targetPath };
const configMap = runtimeGitopsScriptsConfigMap({ overlay, scriptsDir, namespace, configMapName });
writeFileSync(targetPath, `${YAML.stringify(configMap).trimEnd()}\n`, "utf8");
return { name: configMapName, namespace, keyCount: Object.keys(configMap.data).length, path: targetPath };
}
function pipelineNamespace(docs) {
@@ -0,0 +1,39 @@
import { readFileSync } from "node:fs";
import path from "node:path";
export function runtimeGitopsScriptsConfigMap({ overlay, scriptsDir, namespace, configMapName }) {
const data = {
"runtime-gitops-overlay.json": `${JSON.stringify({
runtimePath: requiredString(overlay.runtimePath, "overlay.runtimePath"),
observability: objectOr(overlay.observability),
})}\n`,
};
for (const name of ["runtime-gitops-observability.mjs", "runtime-gitops-postprocess.mjs", "runtime-gitops-verify.mjs"]) {
data[name] = readFileSync(path.join(scriptsDir, name), "utf8");
}
return {
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: configMapName,
namespace,
labels: {
"app.kubernetes.io/name": "hwlab-runtime-gitops-scripts",
"app.kubernetes.io/part-of": "unidesk-hwlab-control-plane",
"app.kubernetes.io/managed-by": "unidesk-host-gitops",
"hwlab.pikastech.local/node": overlay.nodeId ?? null,
"hwlab.pikastech.local/lane": overlay.lane ?? null,
},
},
data,
};
}
function requiredString(value, pathLabel) {
if (typeof value !== "string" || value.length === 0) throw new Error(`${pathLabel} must be a non-empty string`);
return value;
}
function objectOr(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}