diff --git a/scripts/src/hwlab-node/render.ts b/scripts/src/hwlab-node/render.ts index a52e37d6..4eae81f0 100644 --- a/scripts/src/hwlab-node/render.ts +++ b/scripts/src/hwlab-node/render.ts @@ -1781,6 +1781,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " const configKey = String(exposure.secretKey || 'frpc.toml');", " const tokenKey = String(exposure.tokenKey || 'token');", " const toml = renderPublicExposureFrpcToml(exposure);", + " const tomlSha256 = crypto.createHash('sha256').update(toml).digest('hex');", " let changed = false;", " let foundConfigMap = false;", " let foundDeployment = false;", @@ -1798,6 +1799,11 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " item.spec = item.spec || {};", " const nextStrategy = { type: 'Recreate' };", " if (JSON.stringify(item.spec.strategy || {}) !== JSON.stringify(nextStrategy)) { item.spec.strategy = nextStrategy; changed = true; }", + " const templateMetadata = templateMetadataFor(item);", + " if (templateMetadata) {", + " templateMetadata.annotations = isObject(templateMetadata.annotations) ? templateMetadata.annotations : {};", + " if (templateMetadata.annotations['unidesk.ai/frpc-config-sha256'] !== tomlSha256) { templateMetadata.annotations['unidesk.ai/frpc-config-sha256'] = tomlSha256; changed = true; }", + " }", " const podSpec = podSpecFor(item);", " for (const container of Array.isArray(podSpec && podSpec.containers) ? podSpec.containers : []) {", " if (!isObject(container)) continue;", @@ -1811,7 +1817,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " process.exit(50);", " }", " if (changed) writeYamlDocuments(file, docs);", - " console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: true, applied: true, changed, filePath: file, hostname: exposure.hostname, serverAddr: exposure.serverAddr, serverPort: exposure.serverPort, webProxy: exposure.webProxy.name, apiProxy: exposure.apiProxy.name, extraProxyCount: Array.isArray(exposure.extraProxies) ? exposure.extraProxies.length : 0, configSha256: crypto.createHash('sha256').update(toml).digest('hex') }));", + " console.error(JSON.stringify({ event: 'unidesk-public-exposure-postprocess', ok: true, applied: true, changed, filePath: file, hostname: exposure.hostname, serverAddr: exposure.serverAddr, serverPort: exposure.serverPort, webProxy: exposure.webProxy.name, apiProxy: exposure.apiProxy.name, extraProxyCount: Array.isArray(exposure.extraProxies) ? exposure.extraProxies.length : 0, configSha256: tomlSha256 }));", " return { configured: true, changed, foundConfigMap, foundDeployment };", "}", "const kustomizationChanged = patchKustomization();", @@ -1838,6 +1844,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " return `node - <<'NODE_UNIDESK_RUNTIME_GITOPS_VERIFY'", "const fs = require('fs');", "const path = require('path');", + "const crypto = require('crypto');", "const YAML = require('yaml');", "const overlay = ${runtimeOverlay};", "const runtimePath = String(overlay.runtimePath || '');", @@ -1868,6 +1875,14 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " 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 envValue(container, name) {", " if (!isObject(container) || !Array.isArray(container.env)) return undefined;", " const item = container.env.find((env) => env && env.name === name);", @@ -2021,11 +2036,15 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " if (!deployment) fail('public-exposure-frpc-deployment-missing', { expected: deploymentName });", " const toml = configMap.data && configMap.data[configKey];", " if (typeof toml !== 'string') fail('public-exposure-frpc-config-missing', { expectedKey: configKey });", + " const expectedConfigSha256 = crypto.createHash('sha256').update(toml).digest('hex');", " const expectedProxyTokens = [exposure.webProxy, exposure.apiProxy, ...(Array.isArray(exposure.extraProxies) ? exposure.extraProxies : [])].flatMap((proxy) => [String(proxy.name), String(proxy.remotePort)]);", " for (const expected of [String(exposure.serverAddr), String(exposure.serverPort), ...expectedProxyTokens, 'HWLAB_FRP_TOKEN']) {", " if (!toml.includes(expected)) fail('public-exposure-frpc-config-mismatch', { expected });", " }", " const podSpec = podSpecFor(deployment);", + " const templateMetadata = templateMetadataFor(deployment);", + " const actualConfigSha256 = templateMetadata && templateMetadata.annotations ? templateMetadata.annotations['unidesk.ai/frpc-config-sha256'] : null;", + " if (actualConfigSha256 !== expectedConfigSha256) fail('public-exposure-frpc-config-rollout-hash-mismatch', { expected: expectedConfigSha256, actual: actualConfigSha256 });", " const containers = Array.isArray(podSpec && podSpec.containers) ? podSpec.containers : [];", " const strategyType = deployment.spec && deployment.spec.strategy && deployment.spec.strategy.type;", " if (strategyType !== 'Recreate') fail('public-exposure-frpc-strategy-mismatch', { expected: 'Recreate', actual: strategyType || null });",