From 414bbba866e0eb749ad7363dd1115f2882585da7 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 27 Jun 2026 14:07:19 +0000 Subject: [PATCH] fix: prepare D518 runtime secret sources --- config/hwlab-node-lanes.yaml | 5 + scripts/src/hwlab-node-lanes.ts | 28 +++++ scripts/src/hwlab-node/entry.ts | 17 +++ scripts/src/hwlab-node/public-exposure.ts | 84 ++++++++++++- scripts/src/hwlab-node/secret-scripts.ts | 146 ++++++++++++++++------ 5 files changed, 236 insertions(+), 44 deletions(-) diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 1b41baa9..163f1484 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -641,6 +641,11 @@ lanes: secretKey: password-hash rollout: deployment: hwlab-cloud-api + codeAgentProvider: + secretName: hwlab-v03-code-agent-provider + sourceRef: hwlab/d518-v03-code-agent-provider.env + openaiSourceKey: OPENAI_API_KEY + opencodeSourceKey: OPENCODE_API_KEY publicExposure: mode: pk01-caddy-frp publicBaseUrl: https://hwlab.pikapython.com diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index da903bd8..6a4e4a09 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -295,6 +295,13 @@ export interface HwlabRuntimeBootstrapAdminSpec { readonly rolloutDeployment: string; } +export interface HwlabRuntimeCodeAgentProviderSpec { + readonly secretName: string; + readonly sourceRef: string; + readonly openaiSourceKey?: string; + readonly opencodeSourceKey?: string; +} + export interface HwlabRuntimeLaneSpec { readonly lane: HwlabRuntimeLane; readonly nodeId: string; @@ -332,6 +339,7 @@ export interface HwlabRuntimeLaneSpec { readonly stepEnv: Record; readonly buildkit?: HwlabRuntimeBuildkitSpec; readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec; + readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec; readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec; readonly runtimeStore?: HwlabRuntimeStoreSpec; readonly webProbe?: HwlabRuntimeWebProbeSpec; @@ -380,6 +388,7 @@ interface HwlabLaneConfig { readonly stepEnv: Record; readonly buildkit?: HwlabRuntimeBuildkitSpec; readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec; + readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec; readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec; readonly runtimeStore?: HwlabRuntimeStoreSpec; readonly webProbe?: HwlabRuntimeWebProbeSpec; @@ -614,6 +623,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record): HwlabLa stepEnv: optionalStringRecord(raw.stepEnv, `lanes.${id}.stepEnv`), buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`), bootstrapAdmin: bootstrapAdminConfig(raw.bootstrapAdmin, `lanes.${id}.bootstrapAdmin`), + codeAgentProvider: codeAgentProviderConfig(raw.codeAgentProvider, `lanes.${id}.codeAgentProvider`), externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`), runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`), webProbe: webProbeConfig(raw.webProbe, `lanes.${id}.webProbe`), @@ -635,6 +645,7 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record< stepEnv: mergeOptionalRecord(baseRaw.stepEnv, targetRaw.stepEnv) ?? {}, buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit), bootstrapAdmin: mergeOptionalRecord(baseRaw.bootstrapAdmin, targetRaw.bootstrapAdmin), + codeAgentProvider: mergeOptionalRecord(baseRaw.codeAgentProvider, targetRaw.codeAgentProvider), externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres), runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore), webProbe: mergeOptionalRecord(baseRaw.webProbe, targetRaw.webProbe), @@ -685,6 +696,22 @@ function bootstrapAdminConfig(value: unknown, path: string): HwlabRuntimeBootstr }; } +function codeAgentProviderConfig(value: unknown, path: string): HwlabRuntimeCodeAgentProviderSpec | undefined { + if (value === undefined) return undefined; + const raw = asRecord(value, path); + const openaiSourceKey = optionalStringField(raw, "openaiSourceKey", path); + const opencodeSourceKey = optionalStringField(raw, "opencodeSourceKey", path); + if (openaiSourceKey === undefined && opencodeSourceKey === undefined) { + throw new Error(`${path} must declare at least one of openaiSourceKey or opencodeSourceKey`); + } + return { + secretName: stringField(raw, "secretName", path), + sourceRef: sourceRefField(raw, "sourceRef", path), + ...(openaiSourceKey === undefined ? {} : { openaiSourceKey: secretKeyField(raw, "openaiSourceKey", path) }), + ...(opencodeSourceKey === undefined ? {} : { opencodeSourceKey: secretKeyField(raw, "opencodeSourceKey", path) }), + }; +} + function externalPostgresComponentConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresComponentSpec { const raw = asRecord(value, path); return { @@ -1239,6 +1266,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec { stepEnv: config.stepEnv, ...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }), ...(config.bootstrapAdmin === undefined ? {} : { bootstrapAdmin: config.bootstrapAdmin }), + ...(config.codeAgentProvider === undefined ? {} : { codeAgentProvider: config.codeAgentProvider }), ...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }), ...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }), ...(config.webProbe === undefined ? {} : { webProbe: config.webProbe }), diff --git a/scripts/src/hwlab-node/entry.ts b/scripts/src/hwlab-node/entry.ts index 3a81dfd9..8b9b5adc 100644 --- a/scripts/src/hwlab-node/entry.ts +++ b/scripts/src/hwlab-node/entry.ts @@ -324,6 +324,20 @@ export interface BootstrapAdminPasswordMaterial { error: string | null; } +export interface CodeAgentProviderSecretMaterial { + ok: boolean; + sourceRef: string | null; + sourcePath: string | null; + sourcePresent: boolean; + openaiSourceKey: string | null; + opencodeSourceKey: string | null; + openaiValue: string | null; + opencodeValue: string | null; + openaiFingerprint: string | null; + opencodeFingerprint: string | null; + error: string | null; +} + export interface NodePublicExposureOptions { action: "public-exposure"; node: string; @@ -373,6 +387,9 @@ export interface RuntimeSecretSpec { codeAgentProviderSecret: string; codeAgentProviderSourceNamespace: string; codeAgentProviderSourceSecret: string; + codeAgentProviderSourceRef?: string; + codeAgentProviderOpenaiSourceKey?: string; + codeAgentProviderOpencodeSourceKey?: string; fieldManager: string; } diff --git a/scripts/src/hwlab-node/public-exposure.ts b/scripts/src/hwlab-node/public-exposure.ts index 6db26ba1..6b61a31c 100644 --- a/scripts/src/hwlab-node/public-exposure.ts +++ b/scripts/src/hwlab-node/public-exposure.ts @@ -28,12 +28,12 @@ import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRul import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport"; import type { RenderedCliResult } from "../output"; -import type { NodePublicExposureOptions, NodeSecretOptions, RuntimeSecretSpec } from "./entry"; +import type { CodeAgentProviderSecretMaterial, NodePublicExposureOptions, NodeSecretOptions, RuntimeSecretSpec } from "./entry"; import { BOOTSTRAP_ADMIN_PASSWORD_HASH_KEY, BOOTSTRAP_ADMIN_SOURCE_NAMESPACE, BOOTSTRAP_ADMIN_SOURCE_SECRET, CLOUD_API_DB_KEY, CODE_AGENT_PROVIDER_OPENAI_KEY, CODE_AGENT_PROVIDER_OPENCODE_KEY, CODE_AGENT_PROVIDER_SOURCE_NAMESPACE, CODE_AGENT_PROVIDER_SOURCE_SECRET, MASTER_ADMIN_API_KEY_KEY, OPENFGA_AUTHN_KEY, OPENFGA_DATASTORE_URI_KEY, OPENFGA_POSTGRES_PASSWORD_KEY } from "./entry"; import { transPath } from "./runtime-common"; import { bootstrapAdminSecretScript, cloudApiDbSecretScript, codeAgentProviderSecretScript, masterAdminApiKeySecretScript, obsoletePlatformDbCleanupScript, obsoletePlatformDbStatusFromText, obsoleteSecretCleanupScript, openFgaSecretScript, ownedPostgresCleanupScript, platformDbSecretStatusScript, secretStatusFromText } from "./secret-scripts"; import { assertLane, assertNodeId, compactCommandResult, keyValueLinesFromText, numericField, optionValue, positiveIntegerOption, readMasterAdminApiKey, requiredOption, shellQuote, statusText } from "./utils"; -import { parseEnvFile, readBootstrapAdminSecretMaterial, syncNodeExternalPostgresSecrets } from "./web-probe"; +import { displayRepoPath, localSecretSourcePaths, parseEnvFile, readBootstrapAdminSecretMaterial, shortSecretFingerprint, syncNodeExternalPostgresSecrets } from "./web-probe"; import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes"; export function isSafeWebProbeScriptRunDir(value: string | null): value is string { @@ -242,9 +242,12 @@ export function runtimeSecretSpec(input: { node: string; lane: string }): Runtim obsoleteHwpodDbSecret: `hwpod-${input.lane}-db`, obsoleteHwpodDbName: `hwpod_${input.lane}`, obsoleteHwpodDbUser: `hwpod_${input.lane}_app`, - codeAgentProviderSecret: `${namespace}-code-agent-provider`, + codeAgentProviderSecret: runtimeLaneSpec?.codeAgentProvider?.secretName ?? `${namespace}-code-agent-provider`, codeAgentProviderSourceNamespace: CODE_AGENT_PROVIDER_SOURCE_NAMESPACE, codeAgentProviderSourceSecret: CODE_AGENT_PROVIDER_SOURCE_SECRET, + ...(runtimeLaneSpec?.codeAgentProvider?.sourceRef === undefined ? {} : { codeAgentProviderSourceRef: runtimeLaneSpec.codeAgentProvider.sourceRef }), + ...(runtimeLaneSpec?.codeAgentProvider?.openaiSourceKey === undefined ? {} : { codeAgentProviderOpenaiSourceKey: runtimeLaneSpec.codeAgentProvider.openaiSourceKey }), + ...(runtimeLaneSpec?.codeAgentProvider?.opencodeSourceKey === undefined ? {} : { codeAgentProviderOpencodeSourceKey: runtimeLaneSpec.codeAgentProvider.opencodeSourceKey }), fieldManager: `unidesk-hwlab-node-${input.lane}-secret`, }; } @@ -256,6 +259,9 @@ export function runNodeSecret(options: NodeSecretOptions): Record existsSync(candidate)) ?? paths[0] ?? null; + const openaiSourceKey = spec.codeAgentProviderOpenaiSourceKey ?? null; + const opencodeSourceKey = spec.codeAgentProviderOpencodeSourceKey ?? null; + if (sourcePath === null) { + return { + ok: false, + sourceRef, + sourcePath, + sourcePresent: false, + openaiSourceKey, + opencodeSourceKey, + openaiValue: null, + opencodeValue: null, + openaiFingerprint: null, + opencodeFingerprint: null, + error: "code-agent-provider-source-path-unresolved", + }; + } + if (!existsSync(sourcePath)) { + return { + ok: false, + sourceRef, + sourcePath: displayRepoPath(sourcePath), + sourcePresent: false, + openaiSourceKey, + opencodeSourceKey, + openaiValue: null, + opencodeValue: null, + openaiFingerprint: null, + opencodeFingerprint: null, + error: "code-agent-provider-source-missing", + }; + } + const values = parseEnvFile(readFileSync(sourcePath, "utf8")); + const openaiValue = openaiSourceKey === null ? null : values[openaiSourceKey] ?? null; + const opencodeValue = opencodeSourceKey === null ? null : values[opencodeSourceKey] ?? null; + const ok = (openaiValue !== null && openaiValue.length > 0) || (opencodeValue !== null && opencodeValue.length > 0); + return { + ok, + sourceRef, + sourcePath: displayRepoPath(sourcePath), + sourcePresent: true, + openaiSourceKey, + opencodeSourceKey, + openaiValue, + opencodeValue, + openaiFingerprint: openaiValue === null || openaiValue.length === 0 ? null : shortSecretFingerprint(openaiValue), + opencodeFingerprint: opencodeValue === null || opencodeValue.length === 0 ? null : shortSecretFingerprint(opencodeValue), + error: ok ? null : "code-agent-provider-source-key-missing", + }; +} + export function nextSecretCommand(options: NodeSecretOptions, spec: RuntimeSecretSpec): Record { if (options.action === "cleanup-owned-postgres") { return { ensure: `bun scripts/cli.ts hwlab nodes secret cleanup-owned-postgres --node ${options.node} --lane ${options.lane} --confirm` }; diff --git a/scripts/src/hwlab-node/secret-scripts.ts b/scripts/src/hwlab-node/secret-scripts.ts index 80849417..8e50efc8 100644 --- a/scripts/src/hwlab-node/secret-scripts.ts +++ b/scripts/src/hwlab-node/secret-scripts.ts @@ -28,13 +28,37 @@ import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRul import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport"; import type { RenderedCliResult } from "../output"; -import type { BootstrapAdminSecretMaterial, NodeSecretOptions, RuntimeSecretSpec } from "./entry"; +import type { BootstrapAdminSecretMaterial, CodeAgentProviderSecretMaterial, NodeSecretOptions, RuntimeSecretSpec } from "./entry"; import { CODE_AGENT_PROVIDER_OPENAI_KEY, CODE_AGENT_PROVIDER_OPENCODE_KEY, MASTER_ADMIN_API_KEY_KEY, OPENFGA_AUTHN_KEY, OPENFGA_DATASTORE_URI_KEY, OPENFGA_POSTGRES_PASSWORD_KEY } from "./entry"; import { parseNodeScopedDelegatedOptions } from "./plan"; import { runTransScript, runtimeSecretSpec } from "./public-exposure"; import { compactCommandResult, keyValueLinesFromText, numericField, shellQuote, splitWhitespaceField, statusText } from "./utils"; import { displayRepoPath } from "./web-probe"; +function base64Value(value: string | null | undefined): string { + return Buffer.from(value ?? "", "utf8").toString("base64"); +} + +function shellUrlEncodeFunction(): string[] { + return [ + "urlencode() {", + " value=$1", + " encoded=", + " i=1", + " len=${#value}", + " while [ \"$i\" -le \"$len\" ]; do", + " c=$(printf '%s' \"$value\" | cut -c \"$i\")", + " case \"$c\" in", + " [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.~_-]) encoded=\"$encoded$c\" ;;", + " *) hex=$(printf '%s' \"$c\" | od -An -tx1 | tr -d ' \\n' | tr '[:lower:]' '[:upper:]'); encoded=\"$encoded%$hex\" ;;", + " esac", + " i=$((i + 1))", + " done", + " printf '%s' \"$encoded\"", + "}", + ]; +} + export function runNodeEndpointBridge(options: ReturnType): Record { if (options.dryRun && options.confirm) throw new Error("control-plane allow-endpoint-bridge accepts only one of --dry-run or --confirm"); const dryRun = options.dryRun || !options.confirm; @@ -682,6 +706,7 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec "secret_b64_key() { kubectl -n \"$namespace\" get secret \"$1\" -o \"go-template={{ index .data \\\"$2\\\" }}\" 2>/dev/null || true; }", "decoded_value() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null || true; fi; }", "decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }", + ...shellUrlEncodeFunction(), "parse_database_url() {", " uri=$1", " uri_host= uri_user= uri_database= uri_sslmode= uri_password_present=no", @@ -741,6 +766,8 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec "before_uri_password_present=$uri_password_present", "pg_password=$(decoded_value \"$before_pg_password_b64\")", "postgres_admin_password=$(decoded_value \"$postgres_admin_b64\")", + "expected_datastore_uri_full=", + "if [ -n \"$pg_password\" ]; then expected_datastore_uri_full=\"postgres://$(urlencode \"$db_user\"):$(urlencode \"$pg_password\")@$db_host:5432/$db_name?sslmode=disable\"; fi", "probe_db", "db_role_exists_before=$role_result", "db_database_exists_before=$database_result", @@ -763,11 +790,7 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec " [ \"$before_authn_present\" = yes ] && [ \"$before_authn_bytes\" -gt 0 ] || missing_secret=true", " [ \"$before_uri_present\" = yes ] && [ \"$before_uri_bytes\" -gt 0 ] || missing_secret=true", " [ \"$before_pg_password_present\" = yes ] && [ \"$before_pg_password_bytes\" -gt 0 ] || missing_secret=true", - " expected_datastore_uri=", - " if [ -n \"$pg_password\" ]; then", - " expected_datastore_uri=\"postgres://$db_user:$pg_password@$db_host:5432/$db_name?sslmode=disable\"", - " [ \"$datastore_uri\" = \"$expected_datastore_uri\" ] || missing_secret=true", - " fi", + " if [ -n \"$expected_datastore_uri_full\" ]; then [ \"$datastore_uri\" = \"$expected_datastore_uri_full\" ] || missing_secret=true; fi", " missing_db=false", " [ \"$db_role_exists_before\" = t ] || missing_db=true", " [ \"$db_database_exists_before\" = t ] || missing_db=true", @@ -788,7 +811,7 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec " else", " [ -n \"$authn_value\" ] || authn_value=$(openssl rand -base64 48)", " [ -n \"$pg_password\" ] || pg_password=$(openssl rand -hex 24)", - " datastore_uri=\"postgres://$db_user:$pg_password@$db_host:5432/$db_name?sslmode=disable\"", + " datastore_uri=\"postgres://$(urlencode \"$db_user\"):$(urlencode \"$pg_password\")@$db_host:5432/$db_name?sslmode=disable\"", " kubectl -n \"$namespace\" create secret generic \"$openfga_secret\" --from-literal=\"$authn_key=$authn_value\" --from-literal=\"$datastore_uri_key=$datastore_uri\" --from-literal=\"$postgres_password_key=$pg_password\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -", " apply_exit=$?", " if [ \"$apply_exit\" -eq 0 ]; then", @@ -804,6 +827,11 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec "SQL", " db_ensure_exit=$?", " if [ \"$db_ensure_exit\" -eq 0 ]; then", + " if ! kubectl -n \"$namespace\" get deployment \"$openfga_deployment\" >/dev/null 2>&1; then", + " openfga_image=deployment-not-present", + " action=ensured", + " mutation=true", + " else", " openfga_image=$(kubectl -n \"$namespace\" get deployment \"$openfga_deployment\" -o 'jsonpath={.spec.template.spec.containers[0].image}' 2>/tmp/hwlab-openfga-image.err)", " migrate_job=\"$openfga_deployment-migrate-unidesk-$(date +%s)\"", " tmp=$(mktemp /tmp/hwlab-openfga-migrate.XXXXXX.yaml)", @@ -856,6 +884,7 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec " elif [ -n \"$rollout_status_exit\" ] && [ \"$rollout_status_exit\" != 0 ]; then action=rollout-status-failed", " else action=ensured; mutation=true; fi", " fi", + " fi", " else action=db-ensure-failed; fi", " else action=apply-failed; fi", " fi", @@ -879,10 +908,8 @@ export function openFgaSecretScript(options: NodeSecretOptions, spec: RuntimeSec "after_uri_database=$uri_database", "after_uri_sslmode=$uri_sslmode", "after_uri_password_present=$uri_password_present", - "expected_uri_prefix=\"postgres://$db_user:\"", - "expected_uri_suffix=\"@$db_host:5432/$db_name?sslmode=disable\"", - "case \"$datastore_uri\" in \"$expected_uri_prefix\"*\"$expected_uri_suffix\") before_uri_matches_expected=yes ;; *) before_uri_matches_expected=no ;; esac", - "case \"$after_datastore_uri\" in \"$expected_uri_prefix\"*\"$expected_uri_suffix\") after_uri_matches_expected=yes ;; *) after_uri_matches_expected=no ;; esac", + "case \"$datastore_uri\" in \"$expected_datastore_uri_full\") before_uri_matches_expected=yes ;; *) before_uri_matches_expected=no ;; esac", + "case \"$after_datastore_uri\" in \"$expected_datastore_uri_full\") after_uri_matches_expected=yes ;; *) after_uri_matches_expected=no ;; esac", "probe_db", "db_role_exists_after=$role_result", "db_database_exists_after=$database_result", @@ -1258,13 +1285,25 @@ export function legacyBootstrapAdminSecretScript(options: NodeSecretOptions, spe ].join("\n"); } -export function codeAgentProviderSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec): string { +export function codeAgentProviderSecretScript(options: NodeSecretOptions, spec: RuntimeSecretSpec, material: CodeAgentProviderSecretMaterial | null): string { + const sourceMode = material === null ? "cluster-secret" : "local-source-ref"; return [ "set +e", `namespace=${shellQuote(spec.namespace)}`, `name=${shellQuote(spec.codeAgentProviderSecret)}`, `source_namespace=${shellQuote(spec.codeAgentProviderSourceNamespace)}`, `source_name=${shellQuote(spec.codeAgentProviderSourceSecret)}`, + `source_mode=${shellQuote(sourceMode)}`, + `source_ref=${shellQuote(material?.sourceRef ?? "")}`, + `source_path=${shellQuote(material?.sourcePath ?? "")}`, + `source_present_config=${shellQuote(material?.sourcePresent === true ? "yes" : "no")}`, + `source_error=${shellQuote(material?.error ?? "")}`, + `openai_source_key=${shellQuote(material?.openaiSourceKey ?? "")}`, + `opencode_source_key=${shellQuote(material?.opencodeSourceKey ?? "")}`, + `source_openai_b64_config=${shellQuote(base64Value(material?.openaiValue))}`, + `source_opencode_b64_config=${shellQuote(base64Value(material?.opencodeValue))}`, + `source_openai_fingerprint=${shellQuote(material?.openaiFingerprint ?? "")}`, + `source_opencode_fingerprint=${shellQuote(material?.opencodeFingerprint ?? "")}`, `openai_key=${shellQuote(CODE_AGENT_PROVIDER_OPENAI_KEY)}`, `opencode_key=${shellQuote(CODE_AGENT_PROVIDER_OPENCODE_KEY)}`, `selected_key=${shellQuote(options.key ?? "")}`, @@ -1278,9 +1317,15 @@ export function codeAgentProviderSecretScript(options: NodeSecretOptions, spec: "before_exists=$(secret_exists_flag \"$namespace\" \"$name\")", "before_openai_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$openai_key\")", "before_opencode_b64=$(secret_b64_key \"$namespace\" \"$name\" \"$opencode_key\")", - "source_exists=$(secret_exists_flag \"$source_namespace\" \"$source_name\")", - "source_openai_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$openai_key\")", - "source_opencode_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$opencode_key\")", + "if [ \"$source_mode\" = local-source-ref ]; then", + " source_exists=$source_present_config", + " source_openai_b64=$source_openai_b64_config", + " source_opencode_b64=$source_opencode_b64_config", + "else", + " source_exists=$(secret_exists_flag \"$source_namespace\" \"$source_name\")", + " source_openai_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$openai_key\")", + " source_opencode_b64=$(secret_b64_key \"$source_namespace\" \"$source_name\" \"$opencode_key\")", + "fi", "before_openai_present=$([ -n \"$before_openai_b64\" ] && printf yes || printf no)", "before_opencode_present=$([ -n \"$before_opencode_b64\" ] && printf yes || printf no)", "source_openai_present=$([ -n \"$source_openai_b64\" ] && printf yes || printf no)", @@ -1333,8 +1378,14 @@ export function codeAgentProviderSecretScript(options: NodeSecretOptions, spec: "printf 'namespace\\t%s\\n' \"$namespace\"", "printf 'secret\\t%s\\n' \"$name\"", "printf 'preset\\t%s\\n' \"$preset\"", + "printf 'sourceMode\\t%s\\n' \"$source_mode\"", "printf 'sourceNamespace\\t%s\\n' \"$source_namespace\"", "printf 'sourceSecret\\t%s\\n' \"$source_name\"", + "printf 'sourceRef\\t%s\\n' \"$source_ref\"", + "printf 'sourcePath\\t%s\\n' \"$source_path\"", + "printf 'sourceError\\t%s\\n' \"$source_error\"", + "printf 'openaiSourceKey\\t%s\\n' \"$openai_source_key\"", + "printf 'opencodeSourceKey\\t%s\\n' \"$opencode_source_key\"", "printf 'selectedKey\\t%s\\n' \"$selected_key\"", "printf 'action\\t%s\\n' \"$action\"", "printf 'dryRun\\t%s\\n' \"$dry_run\"", @@ -1347,8 +1398,10 @@ export function codeAgentProviderSecretScript(options: NodeSecretOptions, spec: "printf 'sourceExists\\t%s\\n' \"$source_exists\"", "printf 'sourceOpenaiPresent\\t%s\\n' \"$source_openai_present\"", "printf 'sourceOpenaiBytes\\t%s\\n' \"$source_openai_bytes\"", + "printf 'sourceOpenaiFingerprint\\t%s\\n' \"$source_openai_fingerprint\"", "printf 'sourceOpencodePresent\\t%s\\n' \"$source_opencode_present\"", "printf 'sourceOpencodeBytes\\t%s\\n' \"$source_opencode_bytes\"", + "printf 'sourceOpencodeFingerprint\\t%s\\n' \"$source_opencode_fingerprint\"", "printf 'afterExists\\t%s\\n' \"$after_exists\"", "printf 'afterOpenaiPresent\\t%s\\n' \"$after_openai_present\"", "printf 'afterOpenaiBytes\\t%s\\n' \"$after_openai_bytes\"", @@ -1381,6 +1434,7 @@ export function cloudApiDbSecretScript(options: NodeSecretOptions, spec: Runtime "secret_b64_key() { kubectl -n \"$namespace\" get secret \"$1\" -o \"go-template={{ index .data \\\"$2\\\" }}\" 2>/dev/null || true; }", "decoded_value() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null || true; fi; }", "decoded_length() { if [ -n \"$1\" ]; then printf '%s' \"$1\" | base64 -d 2>/dev/null | wc -c | tr -d ' '; else printf '0'; fi; }", + ...shellUrlEncodeFunction(), "parse_database_url() {", " uri=$1", " uri_host= uri_user= uri_database= uri_sslmode= uri_password_present=no", @@ -1443,6 +1497,8 @@ export function cloudApiDbSecretScript(options: NodeSecretOptions, spec: Runtime "postgres_admin_b64=$(secret_b64_key \"$postgres_secret\" POSTGRES_PASSWORD)", "postgres_admin_present=$([ -n \"$postgres_admin_b64\" ] && printf yes || printf no)", "postgres_admin_password=$(decoded_value \"$postgres_admin_b64\")", + "expected_database_url_full=", + "if [ -n \"$postgres_admin_password\" ]; then expected_database_url_full=\"postgres://$(urlencode \"$db_user\"):$(urlencode \"$postgres_admin_password\")@$db_host:5432/$db_name?sslmode=disable\"; fi", "probe_db", "db_role_exists_before=$role_result", "db_database_exists_before=$database_result", @@ -1465,11 +1521,7 @@ export function cloudApiDbSecretScript(options: NodeSecretOptions, spec: Runtime "if [ \"$action_request\" = ensure ]; then", " missing_secret=false", " [ \"$before_url_present\" = yes ] && [ \"$before_url_bytes\" -gt 0 ] || missing_secret=true", - " expected_database_url=", - " if [ -n \"$postgres_admin_password\" ]; then", - " expected_database_url=\"postgres://$db_user:$postgres_admin_password@$db_host:5432/$db_name?sslmode=disable\"", - " [ \"$database_url\" = \"$expected_database_url\" ] || missing_secret=true", - " fi", + " if [ -n \"$expected_database_url_full\" ]; then [ \"$database_url\" = \"$expected_database_url_full\" ] || missing_secret=true; fi", " missing_db=false", " [ \"$db_role_exists_before\" = t ] || missing_db=true", " [ \"$db_database_exists_before\" = t ] || missing_db=true", @@ -1481,7 +1533,7 @@ export function cloudApiDbSecretScript(options: NodeSecretOptions, spec: Runtime " elif [ \"$missing_secret\" = false ] && [ \"$missing_db\" = false ] && [ \"$consumer_not_ready\" = false ]; then", " action=kept", " else", - " database_url=\"postgres://$db_user:$postgres_admin_password@$db_host:5432/$db_name?sslmode=disable\"", + " database_url=\"postgres://$(urlencode \"$db_user\"):$(urlencode \"$postgres_admin_password\")@$db_host:5432/$db_name?sslmode=disable\"", " kubectl -n \"$namespace\" create secret generic \"$name\" --from-literal=\"$database_url_key=$database_url\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=\"$field_manager\" -f -", " apply_exit=$?", " if [ \"$apply_exit\" -eq 0 ]; then", @@ -1504,14 +1556,7 @@ export function cloudApiDbSecretScript(options: NodeSecretOptions, spec: Runtime " rc=$?", " if [ \"$rc\" -ne 0 ]; then rollout_restart_exit=$rc; break; fi", " done", - " if [ \"$rollout_restart_exit\" -eq 0 ]; then", - " rollout_status_exit=0", - " for deployment in $db_consumer_deployments; do", - " kubectl -n \"$namespace\" rollout status \"deployment/$deployment\" --timeout=180s >/tmp/hwlab-db-consumer-rollout-status-$deployment.out 2>/tmp/hwlab-db-consumer-rollout-status-$deployment.err", - " rc=$?", - " if [ \"$rc\" -ne 0 ]; then rollout_status_exit=$rc; break; fi", - " done", - " fi", + " if [ \"$rollout_restart_exit\" -eq 0 ]; then rollout_status_exit=0; fi", " fi", " if [ -n \"$rollout_restart_exit\" ] && [ \"$rollout_restart_exit\" != 0 ]; then action=rollout-restart-failed", " elif [ -n \"$rollout_status_exit\" ] && [ \"$rollout_status_exit\" != 0 ]; then action=rollout-status-failed", @@ -1531,10 +1576,8 @@ export function cloudApiDbSecretScript(options: NodeSecretOptions, spec: Runtime "after_url_database=$uri_database", "after_url_sslmode=$uri_sslmode", "after_url_password_present=$uri_password_present", - "expected_url_prefix=\"postgres://$db_user:\"", - "expected_url_suffix=\"@$db_host:5432/$db_name?sslmode=disable\"", - "case \"$database_url\" in \"$expected_url_prefix\"*\"$expected_url_suffix\") before_url_matches_expected=yes ;; *) before_url_matches_expected=no ;; esac", - "case \"$after_database_url\" in \"$expected_url_prefix\"*\"$expected_url_suffix\") after_url_matches_expected=yes ;; *) after_url_matches_expected=no ;; esac", + "case \"$database_url\" in \"$expected_database_url_full\") before_url_matches_expected=yes ;; *) before_url_matches_expected=no ;; esac", + "case \"$after_database_url\" in \"$expected_database_url_full\") after_url_matches_expected=yes ;; *) after_url_matches_expected=no ;; esac", "probe_db", "db_role_exists_after=$role_result", "db_database_exists_after=$database_result", @@ -1790,18 +1833,41 @@ export function secretStatusFromText(text: string, commandOk: boolean, exitCode: const openaiReady = fields.afterOpenaiPresent === "yes" && typeof afterOpenaiBytes === "number" && afterOpenaiBytes > 0; const opencodeReady = fields.afterOpencodePresent === "yes" && typeof afterOpencodeBytes === "number" && afterOpencodeBytes > 0; const healthy = fields.afterExists === "yes" && (openaiReady || opencodeReady); + const localSourceMode = fields.sourceMode === "local-source-ref"; return { ok: commandOk && healthy, namespace: fields.namespace || spec.namespace, secret: fields.secret || spec.codeAgentProviderSecret, preset: "code-agent-provider", - source: { - namespace: fields.sourceNamespace || spec.codeAgentProviderSourceNamespace, - secret: fields.sourceSecret || spec.codeAgentProviderSourceSecret, - exists: fields.sourceExists === "yes", - openaiApiKey: { keyPresent: fields.sourceOpenaiPresent === "yes", valueBytes: sourceOpenaiBytes }, - opencodeApiKey: { keyPresent: fields.sourceOpencodePresent === "yes", valueBytes: sourceOpencodeBytes }, - }, + source: localSourceMode + ? { + mode: "local-source-ref", + sourceRef: fields.sourceRef || spec.codeAgentProviderSourceRef || null, + sourcePath: fields.sourcePath || null, + exists: fields.sourceExists === "yes", + error: fields.sourceError || null, + openaiApiKey: { + sourceKey: fields.openaiSourceKey || spec.codeAgentProviderOpenaiSourceKey || null, + keyPresent: fields.sourceOpenaiPresent === "yes", + valueBytes: sourceOpenaiBytes, + fingerprint: fields.sourceOpenaiFingerprint || null, + }, + opencodeApiKey: { + sourceKey: fields.opencodeSourceKey || spec.codeAgentProviderOpencodeSourceKey || null, + keyPresent: fields.sourceOpencodePresent === "yes", + valueBytes: sourceOpencodeBytes, + fingerprint: fields.sourceOpencodeFingerprint || null, + }, + valuesRedacted: true, + } + : { + mode: "cluster-secret", + namespace: fields.sourceNamespace || spec.codeAgentProviderSourceNamespace, + secret: fields.sourceSecret || spec.codeAgentProviderSourceSecret, + exists: fields.sourceExists === "yes", + openaiApiKey: { keyPresent: fields.sourceOpenaiPresent === "yes", valueBytes: sourceOpenaiBytes }, + opencodeApiKey: { keyPresent: fields.sourceOpencodePresent === "yes", valueBytes: sourceOpencodeBytes }, + }, selectedKey: fields.selectedKey || null, action: fields.action || null, dryRun: fields.dryRun === "true",