fix: 收敛 HWLAB runtime GitOps ConfigMap ownership
This commit is contained in:
@@ -58,6 +58,12 @@ delivery:
|
||||
branch: unidesk-host-gitops
|
||||
manifestPath: deploy/gitops/unidesk-host/todo-note.yaml
|
||||
releaseStatePath: deploy/gitops-state/unidesk-host/todo-note.json
|
||||
resources:
|
||||
- id: hwlab-nc01-v03-runtime-gitops-scripts
|
||||
renderer: hwlab-runtime-gitops-scripts
|
||||
configRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01
|
||||
manifestPath: deploy/gitops/unidesk-host/hwlab-nc01-v03-runtime-gitops-scripts.yaml
|
||||
namespace: hwlab-ci
|
||||
author:
|
||||
name: UniDesk Host CI
|
||||
email: unidesk-host-ci@unidesk.local
|
||||
|
||||
@@ -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 : {};
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import type { CicdDeliveryAuthority } from "../cicd-delivery-authority";
|
||||
|
||||
const runtimeGitopsObservabilityNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-observability.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsPipelineGuardNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-pipeline-guard.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsScriptsConfigMapNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsPostprocessNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-postprocess.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsVerifyNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-verify.mjs"), "utf8").trimEnd();
|
||||
const runtimePipelineProvenanceNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-pipeline-provenance.mjs"), "utf8").trimEnd();
|
||||
@@ -2927,6 +2928,7 @@ function runtimeGitopsPipelineGuardScript(): string[] {
|
||||
"runtime_gitops_guard_dir=\"$render_dir/.unidesk-runtime-gitops\"",
|
||||
"mkdir -p \"$runtime_gitops_guard_dir\"",
|
||||
...writeRuntimeGitopsNativeScript("runtime-gitops-pipeline-guard.mjs", runtimeGitopsPipelineGuardNativeScript, "UNIDESK_RUNTIME_GITOPS_PIPELINE_GUARD_MJS"),
|
||||
...writeRuntimeGitopsNativeScript("runtime-gitops-scripts-configmap.mjs", runtimeGitopsScriptsConfigMapNativeScript, "UNIDESK_RUNTIME_GITOPS_SCRIPTS_CONFIGMAP_MJS"),
|
||||
...writeRuntimeGitopsNativeScript("runtime-gitops-observability.mjs", runtimeGitopsObservabilityNativeScript, "UNIDESK_RUNTIME_GITOPS_OBSERVABILITY_MJS"),
|
||||
...writeRuntimeGitopsNativeScript("runtime-gitops-postprocess.mjs", runtimeGitopsPostprocessNativeScript, "UNIDESK_RUNTIME_GITOPS_POSTPROCESS_MJS"),
|
||||
...writeRuntimeGitopsNativeScript("runtime-gitops-verify.mjs", runtimeGitopsVerifyNativeScript, "UNIDESK_RUNTIME_GITOPS_VERIFY_MJS"),
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
observePacSourceArtifactRuntime,
|
||||
selectPipelineRunBySourceCommit,
|
||||
} from "../native/cicd/pac-source-artifact-runtime.mjs";
|
||||
import { renderGitOpsResources } from "../native/cicd/publish-unidesk-host-gitops.mjs";
|
||||
|
||||
const sourceCommit = "a".repeat(40);
|
||||
const provenance = {
|
||||
@@ -306,6 +307,7 @@ describe("HWLAB YAML-owned Pipeline provenance", () => {
|
||||
expect(scripts.join("\n")).not.toContain("UNIDESK_RUNTIME_GITOPS_OVERLAY_B64=");
|
||||
expect(scripts.join("\n")).not.toContain(Buffer.from(JSON.stringify(overlay.observability), "utf8").toString("base64"));
|
||||
const configMap = Bun.YAML.parse(readFileSync(configMapPath, "utf8")) as Record<string, any>;
|
||||
expect(configMap.metadata.labels["app.kubernetes.io/managed-by"]).toBe("unidesk-host-gitops");
|
||||
expect(JSON.parse(configMap.data["runtime-gitops-overlay.json"])).toEqual({ runtimePath: overlay.runtimePath, observability: overlay.observability });
|
||||
expect(Object.keys(configMap.data).sort()).toEqual([
|
||||
"runtime-gitops-observability.mjs",
|
||||
@@ -313,6 +315,23 @@ describe("HWLAB YAML-owned Pipeline provenance", () => {
|
||||
"runtime-gitops-postprocess.mjs",
|
||||
"runtime-gitops-verify.mjs",
|
||||
]);
|
||||
const hostConfig = Bun.YAML.parse(readFileSync(rootPath("config", "unidesk-host-k8s.yaml"), "utf8")) as Record<string, any>;
|
||||
expect(hostConfig.delivery.gitops.resources).toContainEqual({
|
||||
id: "hwlab-nc01-v03-runtime-gitops-scripts",
|
||||
renderer: "hwlab-runtime-gitops-scripts",
|
||||
configRef: "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01",
|
||||
manifestPath: "deploy/gitops/unidesk-host/hwlab-nc01-v03-runtime-gitops-scripts.yaml",
|
||||
namespace: "hwlab-ci",
|
||||
});
|
||||
const gitopsResources = renderGitOpsResources(hostConfig.delivery.gitops, rootPath());
|
||||
expect(gitopsResources).toHaveLength(1);
|
||||
expect(gitopsResources[0]?.path).toBe("deploy/gitops/unidesk-host/hwlab-nc01-v03-runtime-gitops-scripts.yaml");
|
||||
const gitopsConfigMap = Bun.YAML.parse(gitopsResources[0]?.content ?? "") as Record<string, any>;
|
||||
expect(gitopsConfigMap.metadata.name).toBe("hwlab-nc01-v03-ci-image-publish-runtime-gitops-scripts");
|
||||
expect(gitopsConfigMap.metadata.namespace).toBe("hwlab-ci");
|
||||
expect(gitopsConfigMap.data["runtime-gitops-postprocess.mjs"]).toContain("UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE");
|
||||
expect(gitopsConfigMap.data["runtime-gitops-postprocess.mjs"].indexOf("UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE"))
|
||||
.toBeLessThan(gitopsConfigMap.data["runtime-gitops-postprocess.mjs"].indexOf("UNIDESK_RUNTIME_GITOPS_OVERLAY_B64"));
|
||||
const legacyPayload = Buffer.from(JSON.stringify({ runtimePath: overlay.runtimePath, observability: overlay.observability }), "utf8").toString("base64");
|
||||
const legacyPipeline = Bun.YAML.parse(readFileSync(pipelinePath, "utf8")) as Record<string, any>;
|
||||
const legacyStep = legacyPipeline.spec.tasks[0].taskSpec.steps[0];
|
||||
|
||||
Reference in New Issue
Block a user