From aedc30c7c1bc0c648870f4bc3a8096dcc6cf39d9 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 27 Jun 2026 13:33:45 +0000 Subject: [PATCH] fix: scope v03 secret-plane smoke to D601 --- config/hwlab-v03/secrets.yaml | 2 + scripts/gitops-render.mjs | 41 ++++++++++++++++--- scripts/gitops-render.test.ts | 76 ++++++++++++++++++++++++++++++++++- scripts/src/runtime-lane.ts | 11 ++++- 4 files changed, 122 insertions(+), 8 deletions(-) diff --git a/config/hwlab-v03/secrets.yaml b/config/hwlab-v03/secrets.yaml index 51aa71ca..250c379c 100644 --- a/config/hwlab-v03/secrets.yaml +++ b/config/hwlab-v03/secrets.yaml @@ -8,6 +8,8 @@ metadata: secretPlane: enabled: true issue: pikasTech/HWLAB#2234 + enabledOnNodes: + - D601 valuesPrinted: false store: kind: ClusterSecretStore diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index 45851fe3..a6e03e99 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -1292,6 +1292,7 @@ function mergeEnvMaps(...values) { function runtimeNodeIdForProfile(deploy, profile, fallback = "node") { if (!isRuntimeLane(profile)) return fallback; + if (typeof fallback === "string" && fallback.trim() && fallback !== "node") return fallback; const nodeId = deploy?.lanes?.[profile]?.node; return typeof nodeId === "string" && nodeId.length > 0 ? nodeId : fallback; } @@ -1357,7 +1358,7 @@ function upsertV02MetricsPort(service) { service.spec.ports.push({ name: "metrics", port: 9100, targetPort: "metrics" }); } -function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = defaultBranch, gitReadUrl, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", useDeployImages = false, metricsSidecarSha256 = null }) { +function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = defaultBranch, gitReadUrl, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", nodeId = "node", useDeployImages = false, metricsSidecarSha256 = null }) { const result = cloneJson(workloads); const deployServices = deployServicesForProfile(deploy, profile); const namespace = namespaceNameForProfile(profile); @@ -1486,7 +1487,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, sourceBranch = applyServiceShutdownDrainLifecycle(container, deployService.shutdownDrain); upsertEnv(container.env, "HWLAB_M3_IO_CONTROL_ENABLED", "false"); upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_REPO_URL", gitReadUrl ?? defaultRuntimeLaneGitReadUrl); - upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", runtimeNodeIdForProfile(deploy, profile)); + upsertEnv(container.env, "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID", runtimeNodeIdForProfile(deploy, profile, nodeId)); upsertEnv(container.env, "HWLAB_OPENFGA_MODE", "enforce"); upsertEnv(container.env, "HWLAB_OPENFGA_API_URL", `http://hwlab-openfga.${namespace}.svc.cluster.local:8080`); upsertEnvEntry(container.env, deployEnvEntry("HWLAB_OPENFGA_AUTHN_TOKEN", `secretRef:hwlab-${profile}-openfga/authn-preshared-key`, namespace, profile)); @@ -5521,7 +5522,7 @@ function runtimeConfigMapsManifest({ configMaps, namespace, labels, annotations }; } -async function runtimeSecretPlaneConfigForProfile(deploy, profile) { +async function runtimeSecretPlaneConfigForProfile(deploy, profile, args = {}) { if (!isRuntimeLane(profile)) return null; const laneConfig = deploy?.lanes?.[profile]; const secretPlane = laneConfig?.secretPlaneRef @@ -5529,9 +5530,39 @@ async function runtimeSecretPlaneConfigForProfile(deploy, profile) { : laneConfig?.secretPlane; if (!secretPlane || typeof secretPlane !== "object" || Array.isArray(secretPlane)) return null; if (secretPlane.enabled !== true) return null; + if (!runtimeSecretPlaneEnabledForNode(secretPlane, args)) return null; return cloneJson(secretPlane); } +function runtimeSecretPlaneEnabledForNode(secretPlane, args = {}) { + const enabledOnNodes = normalizeSecretPlaneNodeList(secretPlane.enabledOnNodes, "secretPlane.enabledOnNodes"); + if (enabledOnNodes.length === 0) return true; + const nodeId = effectiveSecretPlaneNodeId(args); + assert.ok(nodeId, "secretPlane.enabledOnNodes requires --node or a node-scoped --gitops-root"); + return enabledOnNodes.includes(nodeId); +} + +function normalizeSecretPlaneNodeList(value, label) { + if (value === undefined) return []; + assert.ok(Array.isArray(value), `${label} must be an array when set`); + return value.map((item, index) => { + assert.equal(typeof item, "string", `${label}[${index}] must be a string`); + const nodeId = item.trim().toUpperCase(); + assert.ok(/^[A-Z0-9][A-Z0-9-]*$/u.test(nodeId), `${label}[${index}] must be a node id`); + return nodeId; + }); +} + +function effectiveSecretPlaneNodeId(args = {}) { + return gitopsRootNodeId(args.gitopsRoot) ?? String(args.nodeId ?? "").trim().toUpperCase(); +} + +function gitopsRootNodeId(gitopsRoot) { + const normalized = String(gitopsRoot ?? "").replaceAll("\\", "/").replace(/\/+$/u, ""); + const match = normalized.match(/(?:^|\/)deploy\/gitops\/node\/([^/]+)$/u); + return match?.[1] ? match[1].trim().toUpperCase() : null; +} + function runtimeSecretPlaneManifest({ config, namespace, labels, annotations }) { const store = config?.store; assert.ok(store && typeof store === "object" && !Array.isArray(store), "secretPlane.store must be an object"); @@ -5744,7 +5775,7 @@ async function plannedFiles(args) { const externalPostgres = externalPostgresConfigForLane(deploy, profile); const workbenchRedis = workbenchRuntimeRedisConfigForProfile(deploy, profile); const runtimeConfigMaps = runtimeConfigMapsForProfile(deploy, profile); - const runtimeSecretPlane = await runtimeSecretPlaneConfigForProfile(deploy, profile); + const runtimeSecretPlane = await runtimeSecretPlaneConfigForProfile(deploy, profile, args); const profileLabels = { ...baseLabels, "hwlab.pikastech.local/environment": runtimeLabelForProfile(profile), "hwlab.pikastech.local/profile": runtimeLabelForProfile(profile) }; const runtimeNamespace = cloneJson(namespaceTemplate); runtimeNamespace.metadata.name = namespace; @@ -5759,7 +5790,7 @@ async function plannedFiles(args) { putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile)); if (runtimeConfigMaps.length > 0) putJson(`${runtimePath}/configmaps.yaml`, runtimeConfigMapsManifest({ configMaps: runtimeConfigMaps, namespace, labels: profileLabels, annotations })); if (runtimeSecretPlane) putJson(`${runtimePath}/external-secrets.yaml`, runtimeSecretPlaneManifest({ config: runtimeSecretPlane, namespace, labels: profileLabels, annotations })); - putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages, metricsSidecarSha256 })); + putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, sourceBranch: args.sourceBranch, gitReadUrl: args.gitReadUrl, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, nodeId: args.nodeId, useDeployImages: args.useDeployImages, metricsSidecarSha256 })); putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, sourceBranch: args.sourceBranch, sourceRepo: args.sourceRepo, gitReadUrl: args.gitReadUrl, deploy, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages, metricsSidecarSha256 })); if (isRuntimeLane(profile) && externalPostgres) putJson(`${runtimePath}/external-postgres.yaml`, externalPostgresManifest({ profile, config: externalPostgres, source })); if (isRuntimeLane(profile) && !externalPostgres) putJson(`${runtimePath}/postgres.yaml`, v02PostgresManifest({ profile, migrationSql, source, image: runtimePostgresImageForProfile(deploy, profile) })); diff --git a/scripts/gitops-render.test.ts b/scripts/gitops-render.test.ts index 54d4b9ec..a7adacd9 100644 --- a/scripts/gitops-render.test.ts +++ b/scripts/gitops-render.test.ts @@ -549,7 +549,7 @@ test("v03 render keeps node identity as data instead of generated structure", as "scripts/run-bun.mjs", "scripts/gitops-render.mjs", "--lane", "v03", - "--node", "G14", + "--node", "D601", "--catalog-path", staleCatalogPath, "--image-tag-mode", "full", "--source-branch", "v0.3", @@ -630,10 +630,82 @@ test("v03 render keeps node identity as data instead of generated structure", as .flatMap((container) => container.env ?? []) .filter((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID"); assert.ok(agentRunNodeEnv.length > 0, "expected AgentRun provider id env to carry configured node id"); - assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["G14"]); + assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["D601"]); } finally { await rm(outDir, { recursive: true, force: true }); await rm(scratchDir, { recursive: true, force: true }); } }); + +test("v03 render skips D601 secret-plane smoke on D518 gitops root", async () => { + const outDir = await mkdtemp(path.join(os.tmpdir(), "hwlab-v03-d518-render-test-")); + const scratchDir = path.join(".tmp", `gitops-render-v03-d518-${process.pid}-${Date.now()}`); + const staleCatalogPath = path.join(scratchDir, "artifact-catalog.v03.json"); + try { + const staleRevision = "0".repeat(40); + await mkdir(scratchDir, { recursive: true }); + await writeFile(staleCatalogPath, JSON.stringify({ + catalogVersion: "v1", + kind: "hwlab-artifact-catalog", + environment: "v03", + profile: "v03", + namespace: "hwlab-v03", + commitId: staleRevision.slice(0, 12), + artifactState: "ready", + services: [{ + serviceId: "hwlab-cloud-api", + runtimeMode: "env-reuse-git-mirror-checkout", + envReuse: true, + sourceCommitId: staleRevision, + commitId: staleRevision, + image: "127.0.0.1:5000/hwlab/hwlab-cloud-api-env:env-stale", + imageTag: "env-stale", + digest: `sha256:${"1".repeat(64)}`, + environmentImage: "127.0.0.1:5000/hwlab/hwlab-cloud-api-env:env-stale", + environmentDigest: `sha256:${"1".repeat(64)}`, + environmentInputHash: "e".repeat(64), + codeInputHash: "c".repeat(64), + bootRepo: "git@github.com:pikasTech/HWLAB.git", + bootCommit: staleRevision, + bootSh: "deploy/runtime/boot/hwlab-cloud-api.sh" + }] + }, null, 2)); + const render = spawnSync(process.execPath, [ + "scripts/run-bun.mjs", + "scripts/gitops-render.mjs", + "--lane", "v03", + "--catalog-path", staleCatalogPath, + "--image-tag-mode", "full", + "--source-branch", "v0.3", + "--gitops-branch", "v0.3-gitops", + "--gitops-root", "deploy/gitops/node/d518", + "--out", outDir, + "--source-revision", "HEAD" + ], { cwd: process.cwd(), encoding: "utf8" }); + assert.equal(render.status, 0, render.stderr || render.stdout); + + const generatedFiles = await collectGeneratedFiles(outDir); + const generatedPaths = generatedFiles.map((filePath) => path.relative(outDir, filePath)); + assert.ok(generatedPaths.includes("runtime-v03/kustomization.yaml")); + assert.ok(generatedPaths.includes("runtime-v03/workloads.yaml")); + assert.equal(generatedPaths.includes("runtime-v03/external-secrets.yaml"), false); + const kustomization = await readFile(path.join(outDir, "runtime-v03", "kustomization.yaml"), "utf8"); + assert.doesNotMatch(kustomization, /external-secrets\.yaml/u); + + const workloadsJson = JSON.parse(await readFile(path.join(outDir, "runtime-v03", "workloads.yaml"), "utf8")); + const cloudApi = (workloadsJson.items ?? []).find((item) => item.kind === "Deployment" && item.metadata?.name === "hwlab-cloud-api"); + const cloudApiContainer = collectContainersFromItem(cloudApi).find((container) => container.name === "hwlab-cloud-api"); + const cloudApiEnvEntries = new Map((cloudApiContainer?.env ?? []).map((entry) => [entry.name, entry])); + assert.deepEqual(cloudApiEnvEntries.get("HWLAB_SECRET_PLANE_SMOKE")?.valueFrom?.secretKeyRef, { name: "hwlab-secret-plane-smoke", key: "password", optional: true }); + const agentRunNodeEnv = (workloadsJson.items ?? []) + .flatMap((item) => collectContainersFromItem(item)) + .flatMap((container) => container.env ?? []) + .filter((entry) => entry.name === "HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID"); + assert.ok(agentRunNodeEnv.length > 0, "expected D518 node id to be inferred from gitops root"); + assert.deepEqual([...new Set(agentRunNodeEnv.map((entry) => entry.value))], ["D518"]); + } finally { + await rm(outDir, { recursive: true, force: true }); + await rm(scratchDir, { recursive: true, force: true }); + } +}); diff --git a/scripts/src/runtime-lane.ts b/scripts/src/runtime-lane.ts index 4df980b1..6feb5555 100644 --- a/scripts/src/runtime-lane.ts +++ b/scripts/src/runtime-lane.ts @@ -142,7 +142,9 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep if (!isRuntimeLane(args.lane)) return args; const result: GitOpsRenderArgs = { ...args }; const laneConfig = runtimeLaneConfig(deploy, args.lane); - const nodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults); + const configuredNodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults); + const rootNodeId = nodeIdFromGitopsRoot(result.gitopsRoot); + const nodeId = result.nodeId && result.nodeId !== defaultNodeId ? result.nodeId : rootNodeId ?? configuredNodeId; const node = nodeConfig(deploy, nodeId); const versionName = versionNameForRuntimeLane(args.lane); const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {}; @@ -172,6 +174,13 @@ export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: Dep return result; } +function nodeIdFromGitopsRoot(gitopsRoot: unknown): string | undefined { + if (typeof gitopsRoot !== "string") return undefined; + const normalized = normalizeRepoPath(gitopsRoot); + const match = normalized.match(/(?:^|\/)deploy\/gitops\/node\/([^/]+)$/u); + return match?.[1] ? match[1].toUpperCase() : undefined; +} + export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] { return Object.entries(deploy.lanes ?? {}) .filter(([lane, config]) => isRuntimeLane(lane) && isObject(config))