fix: patch runtime GitOps YAML workloads
This commit is contained in:
@@ -2,8 +2,12 @@
|
||||
// SPEC: PJ2026-01060703 CI/CD branch follower runtime GitOps postprocess.
|
||||
// Responsibility: mutate rendered runtime GitOps files after HWLAB source render, before publish.
|
||||
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
const requireFromScript = createRequire(import.meta.url);
|
||||
const requireFromCwd = createRequire(path.join(process.cwd(), "package.json"));
|
||||
const YAML = requireYaml();
|
||||
const repoDir = process.cwd();
|
||||
const overlay = readOverlay();
|
||||
const runtimePath = requiredOverlayString("runtimePath");
|
||||
@@ -45,28 +49,29 @@ function patchCodeAgentRuntimeWorkloads() {
|
||||
const kafka = runtime.kafkaEventBridge;
|
||||
let changed = false;
|
||||
for (const file of listYamlFiles(runtimeDir)) {
|
||||
const parsed = parseJsonDocument(readFileSync(file, "utf8"));
|
||||
if (parsed === null) continue;
|
||||
const items = isKubernetesList(parsed) ? parsed.items : [parsed];
|
||||
const documents = parseStructuredDocuments(readFileSync(file, "utf8"), file);
|
||||
let fileChanged = false;
|
||||
for (const item of items) {
|
||||
const workloadName = String(item?.metadata?.labels?.["app.kubernetes.io/name"] ?? item?.metadata?.name ?? "");
|
||||
const containers = item?.spec?.template?.spec?.containers;
|
||||
if (!Array.isArray(containers)) continue;
|
||||
for (const container of containers) {
|
||||
if (!Array.isArray(container?.env)) continue;
|
||||
if (workloadName === "hwlab-cloud-api" && container.name === "hwlab-cloud-api") {
|
||||
fileChanged = patchCloudApiKafkaEnv(container, kafka) || fileChanged;
|
||||
}
|
||||
if (workloadName === "hwlab-cloud-web" && container.name === "hwlab-cloud-web") {
|
||||
fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafka.enabled && kafka.features.liveKafkaSse)) || fileChanged;
|
||||
fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", String(kafka.enabled && kafka.features.kafkaRefreshReplay)) || fileChanged;
|
||||
fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafka.enabled && kafka.features.projectionRealtime)) || fileChanged;
|
||||
for (const document of documents.values) {
|
||||
const items = isKubernetesList(document) ? document.items : [document];
|
||||
for (const item of items) {
|
||||
const workloadName = String(item?.metadata?.labels?.["app.kubernetes.io/name"] ?? item?.metadata?.name ?? "");
|
||||
const containers = item?.spec?.template?.spec?.containers;
|
||||
if (!Array.isArray(containers)) continue;
|
||||
for (const container of containers) {
|
||||
if (!Array.isArray(container?.env)) continue;
|
||||
if (workloadName === "hwlab-cloud-api" && container.name === "hwlab-cloud-api") {
|
||||
fileChanged = patchCloudApiKafkaEnv(container, kafka) || fileChanged;
|
||||
}
|
||||
if (workloadName === "hwlab-cloud-web" && container.name === "hwlab-cloud-web") {
|
||||
fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafka.enabled && kafka.features.liveKafkaSse)) || fileChanged;
|
||||
fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", String(kafka.enabled && kafka.features.kafkaRefreshReplay)) || fileChanged;
|
||||
fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafka.enabled && kafka.features.projectionRealtime)) || fileChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fileChanged) {
|
||||
writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
||||
writeFileSync(file, serializeStructuredDocuments(documents), "utf8");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -237,6 +242,20 @@ function parseJsonDocument(text) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseStructuredDocuments(text, file) {
|
||||
const json = parseJsonDocument(text);
|
||||
if (json !== null) return { format: "json", values: [json] };
|
||||
const documents = YAML.parseAllDocuments(text);
|
||||
const errors = documents.flatMap((document) => document.errors);
|
||||
if (errors.length > 0) throw new Error(`invalid YAML in ${file}: ${errors.map((error) => error.message).join("; ")}`);
|
||||
return { format: "yaml", values: documents.map((document) => document.toJS()).filter((document) => document !== null) };
|
||||
}
|
||||
|
||||
function serializeStructuredDocuments(documents) {
|
||||
if (documents.format === "json") return `${JSON.stringify(documents.values[0], null, 2)}\n`;
|
||||
return `${documents.values.map((document) => YAML.stringify(document).trimEnd()).join("\n---\n")}\n`;
|
||||
}
|
||||
|
||||
function leadingSpaces(value) {
|
||||
const match = value.match(/^ */u);
|
||||
return match ? match[0].length : 0;
|
||||
@@ -264,6 +283,14 @@ function readOverlay() {
|
||||
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
|
||||
}
|
||||
|
||||
function requireYaml() {
|
||||
try {
|
||||
return requireFromScript("yaml");
|
||||
} catch {
|
||||
return requireFromCwd("yaml");
|
||||
}
|
||||
}
|
||||
|
||||
function requiredOverlayString(name) {
|
||||
const value = overlay[name];
|
||||
if (typeof value !== "string" || value.length === 0) throw new Error(`overlay.${name} is required`);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const roots: string[] = [];
|
||||
const script = resolve(import.meta.dir, "runtime-gitops-postprocess.mjs");
|
||||
|
||||
afterEach(() => {
|
||||
while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("patches YAML workloads across ordinary, multi-document, and Kubernetes List files", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "runtime-gitops-postprocess-"));
|
||||
roots.push(root);
|
||||
const runtimeDir = join(root, "runtime");
|
||||
mkdirSync(runtimeDir);
|
||||
writeFileSync(join(root, "overlay.json"), JSON.stringify(overlay("runtime")));
|
||||
writeFileSync(join(runtimeDir, "cloud-api.yaml"), deploymentYaml("hwlab-cloud-api"));
|
||||
writeFileSync(join(runtimeDir, "cloud-web.yaml"), `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: unchanged\n---\n${deploymentYaml("hwlab-cloud-web")}`);
|
||||
writeFileSync(join(runtimeDir, "list.yaml"), `apiVersion: v1\nkind: List\nitems:\n${listItem(deploymentYaml("hwlab-cloud-api"))}\n`);
|
||||
writeFileSync(join(runtimeDir, "json-workload.yaml"), `${JSON.stringify(Bun.YAML.parse(deploymentYaml("hwlab-cloud-api")), null, 2)}\n`);
|
||||
|
||||
const result = spawnSync(process.execPath, [script], {
|
||||
cwd: root,
|
||||
env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") },
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
if (result.status !== 0) throw new Error(result.stderr);
|
||||
expect(result.stderr).toContain('"codeAgentRuntimeChanged":true');
|
||||
|
||||
const api = Bun.YAML.parse(readFileSync(join(runtimeDir, "cloud-api.yaml"), "utf8")) as any;
|
||||
expect(authorityValues(api)).toEqual(["false", "false", "false", "true", "true", "true"]);
|
||||
|
||||
const multiDocs = readFileSync(join(runtimeDir, "cloud-web.yaml"), "utf8").split(/^---$/mu).map((document) => Bun.YAML.parse(document) as any);
|
||||
expect(multiDocs[0].kind).toBe("ConfigMap");
|
||||
expect(envValues(multiDocs[1])).toMatchObject({
|
||||
HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false",
|
||||
HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false",
|
||||
HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "true",
|
||||
});
|
||||
|
||||
const list = Bun.YAML.parse(readFileSync(join(runtimeDir, "list.yaml"), "utf8")) as any;
|
||||
expect(list.kind).toBe("List");
|
||||
expect(authorityValues(list.items[0])).toEqual(["false", "false", "false", "true", "true", "true"]);
|
||||
|
||||
const jsonText = readFileSync(join(runtimeDir, "json-workload.yaml"), "utf8");
|
||||
expect(jsonText.startsWith("{\n")).toBe(true);
|
||||
expect(authorityValues(JSON.parse(jsonText))).toEqual(["false", "false", "false", "true", "true", "true"]);
|
||||
|
||||
const afterFirstRun = snapshot(runtimeDir);
|
||||
const second = spawnSync(process.execPath, [script], {
|
||||
cwd: root,
|
||||
env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") },
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (second.status !== 0) throw new Error(second.stderr);
|
||||
expect(second.stderr).toContain('"codeAgentRuntimeChanged":false');
|
||||
expect(snapshot(runtimeDir)).toEqual(afterFirstRun);
|
||||
});
|
||||
|
||||
function overlay(runtimePath: string) {
|
||||
return {
|
||||
runtimePath,
|
||||
codeAgentRuntime: {
|
||||
enabled: true,
|
||||
kafkaEventBridge: {
|
||||
enabled: true,
|
||||
directPublishConsumerGroupId: "hwlab-direct",
|
||||
transactionalProjectorConsumerGroupId: "hwlab-projector",
|
||||
features: {
|
||||
directPublish: false,
|
||||
liveKafkaSse: false,
|
||||
kafkaRefreshReplay: false,
|
||||
transactionalProjector: true,
|
||||
projectionOutboxRelay: true,
|
||||
projectionRealtime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function deploymentYaml(name: string) {
|
||||
return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ${name}\n labels:\n app.kubernetes.io/name: ${name}\nspec:\n template:\n spec:\n containers:\n - name: ${name}\n env:\n - name: EXISTING\n value: preserved\n`;
|
||||
}
|
||||
|
||||
function listItem(value: string) {
|
||||
const [first, ...rest] = value.trimEnd().split("\n");
|
||||
return [` - ${first}`, ...rest.map((line) => ` ${line}`)].join("\n");
|
||||
}
|
||||
|
||||
function envValues(workload: any) {
|
||||
return Object.fromEntries(workload.spec.template.spec.containers[0].env.map((item: any) => [item.name, item.value]));
|
||||
}
|
||||
|
||||
function authorityValues(workload: any) {
|
||||
const env = envValues(workload);
|
||||
return [
|
||||
env.HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED,
|
||||
env.HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED,
|
||||
env.HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED,
|
||||
env.HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED,
|
||||
env.HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED,
|
||||
env.HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED,
|
||||
];
|
||||
}
|
||||
|
||||
function snapshot(runtimeDir: string) {
|
||||
return ["cloud-api.yaml", "cloud-web.yaml", "list.yaml", "json-workload.yaml"].map((name) => readFileSync(join(runtimeDir, name), "utf8"));
|
||||
}
|
||||
Reference in New Issue
Block a user