From 04404887937732d180894e43e18bec3cb9feedde Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 12 Jun 2026 21:40:49 +0000 Subject: [PATCH] fix: postprocess D601 HWLAB runtime GitOps --- scripts/src/hwlab-node.ts | 173 +++++++++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 3 deletions(-) diff --git a/scripts/src/hwlab-node.ts b/scripts/src/hwlab-node.ts index c2964aa3..33b9d083 100644 --- a/scripts/src/hwlab-node.ts +++ b/scripts/src/hwlab-node.ts @@ -1352,14 +1352,130 @@ function nodeRuntimePipelinePostprocessScript(): string[] { "function writeYaml(file, doc) { fs.writeFileSync(file, YAML.stringify(doc).trimEnd() + '\\\\n'); }", "function listItems(doc) { return doc && doc.kind === 'List' && Array.isArray(doc.items) ? doc.items : [doc]; }", "function normalizeList(items) { return { apiVersion: 'v1', kind: 'List', items }; }", + "function isObject(value) { return value && typeof value === 'object' && !Array.isArray(value); }", + "function yamlFiles(dir) {", + " const files = [];", + " for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {", + " const file = path.join(dir, entry.name);", + " if (entry.isDirectory()) files.push(...yamlFiles(file));", + " else if (entry.isFile() && /\\.ya?ml$/u.test(entry.name)) files.push(file);", + " }", + " return files;", + "}", + "function readYamlDocuments(file) { return YAML.parseAllDocuments(fs.readFileSync(file, 'utf8')).map((document) => document.toJS()).filter((doc) => doc !== null); }", + "function writeYamlDocuments(file, docs) { fs.writeFileSync(file, docs.map((doc) => YAML.stringify(doc).trimEnd()).join('\\\\n---\\\\n') + '\\\\n'); }", + "function podSpecFor(item) {", + " if (!isObject(item) || !isObject(item.spec)) return null;", + " if (item.kind === 'Pod') return item.spec;", + " if (['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'ReplicationController'].includes(item.kind)) return item.spec.template && item.spec.template.spec ? item.spec.template.spec : null;", + " if (item.kind === 'Job') return item.spec.template && item.spec.template.spec ? item.spec.template.spec : null;", + " if (item.kind === 'CronJob') return item.spec.jobTemplate && item.spec.jobTemplate.spec && item.spec.jobTemplate.spec.template ? item.spec.jobTemplate.spec.template.spec : null;", + " return null;", + "}", + "function templateMetadataFor(item) {", + " if (!isObject(item) || !isObject(item.spec)) return null;", + " if (item.kind === 'Pod') return item.metadata || null;", + " if (['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'ReplicationController'].includes(item.kind)) return item.spec.template ? item.spec.template.metadata : null;", + " if (item.kind === 'Job') return item.spec.template ? item.spec.template.metadata : null;", + " if (item.kind === 'CronJob') return item.spec.jobTemplate && item.spec.jobTemplate.spec && item.spec.jobTemplate.spec.template ? item.spec.jobTemplate.spec.template.metadata : null;", + " return null;", + "}", + "function stripMonitoringMetadata(metadata) {", + " if (!isObject(metadata)) return false;", + " let changed = false;", + " if (isObject(metadata.labels) && metadata.labels['hwlab.pikastech.local/monitoring'] !== undefined && metadata.labels['hwlab.pikastech.local/monitoring'] !== 'disabled') {", + " metadata.labels['hwlab.pikastech.local/monitoring'] = 'disabled';", + " changed = true;", + " }", + " if (isObject(metadata.annotations) && metadata.annotations['hwlab.pikastech.local/metrics-sidecar-sha256'] !== undefined) {", + " delete metadata.annotations['hwlab.pikastech.local/metrics-sidecar-sha256'];", + " changed = true;", + " }", + " return changed;", + "}", + "function containerHasVolumeMount(container, name) { return isObject(container) && Array.isArray(container.volumeMounts) && container.volumeMounts.some((mount) => mount && mount.name === name); }", + "function removeMetricsSidecar(podSpec) {", + " if (!isObject(podSpec)) return false;", + " let changed = false;", + " if (Array.isArray(podSpec.containers)) {", + " const next = podSpec.containers.filter((container) => !(isObject(container) && (container.name === 'hwlab-metrics' || (Array.isArray(container.command) && container.command.includes('/metrics/metrics-sidecar.mjs')))));", + " if (next.length !== podSpec.containers.length) { podSpec.containers = next; changed = true; }", + " }", + " for (const group of ['containers', 'initContainers']) {", + " for (const container of Array.isArray(podSpec[group]) ? podSpec[group] : []) {", + " if (!isObject(container) || !Array.isArray(container.volumeMounts)) continue;", + " const nextMounts = container.volumeMounts.filter((mount) => !(mount && mount.name === 'hwlab-metrics-sidecar'));", + " if (nextMounts.length !== container.volumeMounts.length) { container.volumeMounts = nextMounts; changed = true; }", + " }", + " }", + " if (Array.isArray(podSpec.volumes)) {", + " const nextVolumes = podSpec.volumes.filter((volume) => !(volume && volume.name === 'hwlab-metrics-sidecar'));", + " if (nextVolumes.length !== podSpec.volumes.length) { podSpec.volumes = nextVolumes; changed = true; }", + " }", + " return changed;", + "}", + "function envValue(container, name) {", + " if (!isObject(container) || !Array.isArray(container.env)) return undefined;", + " const item = container.env.find((env) => env && env.name === name);", + " return item ? item.value : undefined;", + "}", + "function isEnvReuseContainer(container) { return envValue(container, 'HWLAB_RUNTIME_MODE') === 'env-reuse-git-mirror-checkout' || envValue(container, 'HWLAB_BOOT_SH') !== undefined || envValue(container, 'HWLAB_BOOT_COMMIT') !== undefined; }", + "function startupProbeFrom(probe) {", + " const next = JSON.parse(JSON.stringify(probe));", + " next.periodSeconds = 10;", + " next.timeoutSeconds = Math.max(Number(next.timeoutSeconds || 0), 2);", + " next.failureThreshold = 30;", + " next.successThreshold = 1;", + " delete next.initialDelaySeconds;", + " return next;", + "}", + "function addEnvReuseStartupProbe(podSpec) {", + " if (!isObject(podSpec) || !Array.isArray(podSpec.containers)) return false;", + " let changed = false;", + " for (const container of podSpec.containers) {", + " if (!isObject(container) || !isEnvReuseContainer(container) || container.startupProbe) continue;", + " const sourceProbe = container.readinessProbe || container.livenessProbe;", + " if (!sourceProbe) continue;", + " container.startupProbe = startupProbeFrom(sourceProbe);", + " changed = true;", + " }", + " return changed;", + "}", + "function patchRuntimeWorkloads() {", + " let observabilityChanged = false;", + " let startupProbeChanged = false;", + " for (const file of yamlFiles(runtimePath)) {", + " if (path.basename(file) === 'kustomization.yaml') continue;", + " const docs = readYamlDocuments(file);", + " let changed = false;", + " for (const doc of docs) {", + " for (const item of listItems(doc).filter(Boolean)) {", + " if (!isObject(item)) continue;", + " if (overlay.observability && overlay.observability.prometheusOperator === false) {", + " const monitoringChanged = stripMonitoringMetadata(item.metadata) || stripMonitoringMetadata(templateMetadataFor(item)) || removeMetricsSidecar(podSpecFor(item));", + " changed = monitoringChanged || changed;", + " observabilityChanged = observabilityChanged || monitoringChanged;", + " }", + " const probeChanged = addEnvReuseStartupProbe(podSpecFor(item));", + " changed = probeChanged || changed;", + " startupProbeChanged = startupProbeChanged || probeChanged;", + " }", + " }", + " if (changed) writeYamlDocuments(file, docs);", + " }", + " return { observabilityChanged, startupProbeChanged };", + "}", "function patchKustomization() {", " const file = path.join(runtimePath, 'kustomization.yaml');", " if (!fs.existsSync(file)) return false;", " const doc = readYaml(file) || {};", " const resources = Array.isArray(doc.resources) ? doc.resources : [];", " const next = resources.filter((item) => !(overlay.observability && overlay.observability.prometheusOperator === false && item === 'observability.yaml'));", - " if (next.length !== resources.length) { doc.resources = next; writeYaml(file, doc); return true; }", - " return false;", + " let changed = false;", + " if (next.length !== resources.length) { doc.resources = next; writeYaml(file, doc); changed = true; }", + " const observabilityFile = path.join(runtimePath, 'observability.yaml');", + " if (overlay.observability && overlay.observability.prometheusOperator === false && fs.existsSync(observabilityFile)) { fs.rmSync(observabilityFile, { force: true }); changed = true; }", + " return changed;", "}", "function patchExternalPostgres() {", " const pg = overlay.externalPostgres;", @@ -1396,8 +1512,9 @@ function nodeRuntimePipelinePostprocessScript(): string[] { " return changed;", "}", "const kustomizationChanged = patchKustomization();", + "const runtimeWorkloadsChanged = patchRuntimeWorkloads();", "const externalPostgresChanged = patchExternalPostgres();", - "console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-postprocess', ok: true, runtimePath, sourcePath, pathRelocated: sourcePath !== runtimePath, observabilityPrometheusOperator: overlay.observability ? overlay.observability.prometheusOperator : null, kustomizationChanged, externalPostgresChanged }));", + "console.error(JSON.stringify({ event: 'unidesk-runtime-gitops-postprocess', ok: true, runtimePath, sourcePath, pathRelocated: sourcePath !== runtimePath, observabilityPrometheusOperator: overlay.observability ? overlay.observability.prometheusOperator : null, kustomizationChanged, observabilityWorkloadsChanged: runtimeWorkloadsChanged.observabilityChanged, startupProbeChanged: runtimeWorkloadsChanged.startupProbeChanged, externalPostgresChanged }));", "NODE_UNIDESK_RUNTIME_GITOPS_POSTPROCESS`;", "}", "function runtimeGitopsVerifyScript() {", @@ -1420,14 +1537,64 @@ function nodeRuntimePipelinePostprocessScript(): string[] { "if (!runtimePath || !fs.existsSync(runtimePath)) fail('runtime-path-missing');", "function readYaml(file) { return YAML.parse(fs.readFileSync(file, 'utf8')); }", "function listItems(doc) { return doc && doc.kind === 'List' && Array.isArray(doc.items) ? doc.items : [doc]; }", + "function isObject(value) { return value && typeof value === 'object' && !Array.isArray(value); }", + "function yamlFiles(dir) {", + " const files = [];", + " for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {", + " const file = path.join(dir, entry.name);", + " if (entry.isDirectory()) files.push(...yamlFiles(file));", + " else if (entry.isFile() && /\\.ya?ml$/u.test(entry.name)) files.push(file);", + " }", + " return files;", + "}", + "function readYamlDocuments(file) { return YAML.parseAllDocuments(fs.readFileSync(file, 'utf8')).map((document) => document.toJS()).filter((doc) => doc !== null); }", + "function podSpecFor(item) {", + " if (!isObject(item) || !isObject(item.spec)) return null;", + " if (item.kind === 'Pod') return item.spec;", + " if (['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet', 'ReplicationController'].includes(item.kind)) return item.spec.template && item.spec.template.spec ? item.spec.template.spec : null;", + " if (item.kind === 'Job') return item.spec.template && item.spec.template.spec ? item.spec.template.spec : null;", + " if (item.kind === 'CronJob') return item.spec.jobTemplate && item.spec.jobTemplate.spec && item.spec.jobTemplate.spec.template ? item.spec.jobTemplate.spec.template.spec : null;", + " return null;", + "}", + "function envValue(container, name) {", + " if (!isObject(container) || !Array.isArray(container.env)) return undefined;", + " const item = container.env.find((env) => env && env.name === name);", + " return item ? item.value : undefined;", + "}", + "function isEnvReuseContainer(container) { return envValue(container, 'HWLAB_RUNTIME_MODE') === 'env-reuse-git-mirror-checkout' || envValue(container, 'HWLAB_BOOT_SH') !== undefined || envValue(container, 'HWLAB_BOOT_COMMIT') !== undefined; }", + "function workloadRef(item, file, container) { return { file, kind: item && item.kind, name: item && item.metadata && item.metadata.name, container: container && container.name }; }", + "function workloadChecks() {", + " const metricsRefs = [];", + " const missingStartupProbes = [];", + " for (const file of yamlFiles(runtimePath)) {", + " if (path.basename(file) === 'kustomization.yaml') continue;", + " for (const doc of readYamlDocuments(file)) {", + " for (const item of listItems(doc).filter(Boolean)) {", + " const podSpec = podSpecFor(item);", + " if (!isObject(podSpec)) continue;", + " for (const container of Array.isArray(podSpec.containers) ? podSpec.containers : []) {", + " if (!isObject(container)) continue;", + " if (container.name === 'hwlab-metrics' || (Array.isArray(container.volumeMounts) && container.volumeMounts.some((mount) => mount && mount.name === 'hwlab-metrics-sidecar'))) metricsRefs.push(workloadRef(item, file, container));", + " if (isEnvReuseContainer(container) && (container.readinessProbe || container.livenessProbe) && !container.startupProbe) missingStartupProbes.push(workloadRef(item, file, container));", + " }", + " if (Array.isArray(podSpec.volumes) && podSpec.volumes.some((volume) => volume && volume.name === 'hwlab-metrics-sidecar')) metricsRefs.push(workloadRef(item, file, { name: 'volume/hwlab-metrics-sidecar' }));", + " }", + " }", + " }", + " return { metricsRefs, missingStartupProbes };", + "}", "const checks = [];", + "const workloadCheck = workloadChecks();", "const kustomizationPath = path.join(runtimePath, 'kustomization.yaml');", "if (overlay.observability && overlay.observability.prometheusOperator === false) {", " if (!fs.existsSync(kustomizationPath)) fail('kustomization-missing');", " const resources = readYaml(kustomizationPath).resources || [];", " if (resources.includes('observability.yaml')) fail('observability-resource-still-rendered', { file: kustomizationPath });", + " if (workloadCheck.metricsRefs.length > 0) fail('observability-sidecar-still-rendered', { refs: workloadCheck.metricsRefs.slice(0, 12), count: workloadCheck.metricsRefs.length });", " checks.push('observability-disabled');", "}", + "if (workloadCheck.missingStartupProbes.length > 0) fail('env-reuse-startup-probe-missing', { refs: workloadCheck.missingStartupProbes.slice(0, 12), count: workloadCheck.missingStartupProbes.length });", + "checks.push('env-reuse-startup-probes');", "const pg = overlay.externalPostgres;", "if (pg && pg.serviceName) {", " const file = path.join(runtimePath, 'external-postgres.yaml');",