From 6e5221f0a3c18abe0a71c124647f519a36668ae3 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 14 Jun 2026 03:47:51 +0000 Subject: [PATCH] fix: sync agentrun v02 runner secrets --- config/agentrun.yaml | 35 ++++++++++++++++++ scripts/src/agentrun-lanes.ts | 11 +++++- scripts/src/agentrun.ts | 70 ++++++++++++++++++++++++++--------- 3 files changed, 96 insertions(+), 20 deletions(-) diff --git a/config/agentrun.yaml b/config/agentrun.yaml index f8a5417a..4c7c0495 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -315,3 +315,38 @@ controlPlane: namespace: agentrun-v02 name: agentrun-v02-api-key key: HWLAB_API_KEY + - id: runner-api-key-legacy-name + sourceRef: /root/.config/hwlab-v02/master-server-admin-api-key.env + sourceKey: HWLAB_API_KEY + targetRef: + namespace: agentrun-v02 + name: agentrun-v01-api-key + key: HWLAB_API_KEY + - id: provider-codex-auth-json + sourceMode: file + sourceRef: /root/.codex/auth.json + targetRef: + namespace: agentrun-v02 + name: agentrun-v01-provider-codex + key: auth.json + - id: provider-codex-config + sourceMode: file + sourceRef: /root/.codex/config.toml + targetRef: + namespace: agentrun-v02 + name: agentrun-v01-provider-codex + key: config.toml + - id: tool-github-pr-token + sourceRef: /root/.config/unidesk/github.env + sourceKey: GH_TOKEN + targetRef: + namespace: agentrun-v02 + name: agentrun-v01-tool-github-pr + key: GH_TOKEN + - id: tool-unidesk-ssh-token + sourceRef: /root/unidesk/.state/docker-compose.env + sourceKey: UNIDESK_SSH_CLIENT_TOKEN + targetRef: + namespace: agentrun-v02 + name: agentrun-v01-tool-unidesk-ssh + key: UNIDESK_SSH_CLIENT_TOKEN diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index b97fa38c..7c5258c3 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -19,7 +19,8 @@ export interface AgentRunSecretRef { export interface AgentRunLaneSecretSpec { readonly id: string; readonly sourceRef: string; - readonly sourceKey: string; + readonly sourceMode: "env" | "file"; + readonly sourceKey: string | null; readonly targetRef: AgentRunSecretRef; } @@ -298,6 +299,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record ({ id: secret.id, sourceRef: secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`, + sourceMode: secret.sourceMode, sourceKey: secret.sourceKey, targetRef: secret.targetRef, valuesPrinted: false, @@ -511,10 +513,15 @@ function parseImageBuild(input: Record, path: string): AgentRun } function parseLaneSecret(input: Record, path: string): AgentRunLaneSecretSpec { + const sourceMode = optionalStringField(input, "sourceMode", path) ?? "env"; + if (sourceMode !== "env" && sourceMode !== "file") throw new Error(`${path}.sourceMode must be env or file`); + const sourceKey = optionalStringField(input, "sourceKey", path) ?? null; + if (sourceMode === "env" && sourceKey === null) throw new Error(`${path}.sourceKey is required when sourceMode is env`); return { id: stringField(input, "id", path), sourceRef: secretSourceRefField(input, "sourceRef", path), - sourceKey: stringField(input, "sourceKey", path), + sourceMode, + sourceKey, targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`), }; } diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index e34e8179..67660dad 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -2339,13 +2339,14 @@ async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Pr valuesPrinted: false, }; } - const values = sources.map((source) => ({ spec: source, value: readSecretSourceValue(spec, source.sourceRef, source.sourceKey) })); + const values = sources.map((source) => ({ spec: source, value: readSecretSourceValue(spec, source) })); const plan = values.map(({ spec: item, value }) => ({ id: item.id, namespace: item.targetRef.namespace, secret: item.targetRef.name, key: item.targetRef.key, sourceRef: item.sourceRef, + sourceMode: item.sourceMode, sourceKey: item.sourceKey, sourcePath: value.redactedPath, fingerprint: value.fingerprint, @@ -3758,15 +3759,32 @@ function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string { ].join("\n"); } -function readSecretSourceValue(spec: AgentRunLaneSpec, sourceRef: string, key: string): { redactedPath: string; value: string; valueBytes: number; fingerprint: string } { +type LaneSecretSource = { + id: string; + sourceRef: string; + sourceMode: "env" | "file"; + sourceKey: string | null; + targetRef: { namespace: string; name: string; key: string }; +}; + +function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource): { redactedPath: string; value: string; valueBytes: number; fingerprint: string } { + const { sourceRef } = source; if (sourceRef.includes("..")) throw new Error(`secret sourceRef must not contain ..: ${sourceRef}`); const sourcePath = sourceRef.startsWith("/") ? sourceRef : join(resolveSecretSourceRoot(spec), ...sourceRef.split("/")); if (!existsSync(sourcePath)) throw new Error(`secret source ${sourceRef} is missing`); - const values = parseEnvFile(readFileSync(sourcePath, "utf8")); - const value = values.get(key); - if (value === undefined || value.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${key}`); + let value: string; + if (source.sourceMode === "file") { + value = readFileSync(sourcePath, "utf8"); + } else { + if (source.sourceKey === null) throw new Error(`secret source ${sourceRef} is missing sourceKey`); + const values = parseEnvFile(readFileSync(sourcePath, "utf8")); + const envValue = values.get(source.sourceKey); + if (envValue === undefined || envValue.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${source.sourceKey}`); + value = envValue; + } + if (value.length === 0) throw new Error(`secret source ${sourceRef} is empty`); return { redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`, value, @@ -3793,8 +3811,9 @@ function resolveSecretSourceRoot(spec: AgentRunLaneSpec): string { function parseEnvFile(text: string): Map { const result = new Map(); for (const rawLine of text.split(/\r?\n/u)) { - const line = rawLine.trim(); + let line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) continue; + if (line.startsWith("export ")) line = line.slice("export ".length).trim(); const index = line.indexOf("="); if (index <= 0) continue; const key = line.slice(0, index).trim(); @@ -3807,12 +3826,13 @@ function parseEnvFile(text: string): Map { return result; } -function collectLaneSecretSources(spec: AgentRunLaneSpec): Array<{ id: string; sourceRef: string; sourceKey: string; targetRef: { namespace: string; name: string; key: string } }> { - const result: Array<{ id: string; sourceRef: string; sourceKey: string; targetRef: { namespace: string; name: string; key: string } }> = []; +function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSource[] { + const result: LaneSecretSource[] = []; if (spec.database.secretSourceRef !== null) { result.push({ id: "database", sourceRef: spec.database.secretSourceRef, + sourceMode: "env", sourceKey: spec.database.secretRef.key, targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key }, }); @@ -3821,6 +3841,7 @@ function collectLaneSecretSources(spec: AgentRunLaneSpec): Array<{ id: string; s result.push({ id: secret.id, sourceRef: secret.sourceRef, + sourceMode: secret.sourceMode, sourceKey: secret.sourceKey, targetRef: secret.targetRef, }); @@ -3852,25 +3873,38 @@ function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ targetRef: { "const crypto = require('node:crypto');", "const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));", "const results = [];", + "const groups = new Map();", "function run(argv, input) {", " const out = cp.spawnSync(argv[0], argv.slice(1), { input, encoding: 'utf8' });", " if (out.status !== 0) throw new Error(`${argv.join(' ')} failed: ${out.stderr || out.stdout}`);", " return out;", "}", + "function tryRun(argv) {", + " return cp.spawnSync(argv[0], argv.slice(1), { encoding: 'utf8' });", + "}", "for (let index = 0; index < items.length; index += 1) {", " const item = items[index];", " const ref = item.targetRef;", - " const value = Buffer.from(item.valueBase64, 'base64');", - " const file = `${process.env.TMP_DIR}/secret-${index}`;", - " fs.writeFileSync(file, value);", - " const ns = run(['kubectl', 'create', 'namespace', ref.namespace, '--dry-run=client', '-o', 'yaml']).stdout;", + " const groupKey = `${ref.namespace}\\u0000${ref.name}`;", + " if (!groups.has(groupKey)) groups.set(groupKey, { namespace: ref.namespace, name: ref.name, items: [] });", + " groups.get(groupKey).items.push({ key: ref.key, valueBase64: item.valueBase64 });", + "}", + "for (const group of groups.values()) {", + " const ns = run(['kubectl', 'create', 'namespace', group.namespace, '--dry-run=client', '-o', 'yaml']).stdout;", " run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns);", - " const secret = run(['kubectl', '-n', ref.namespace, 'create', 'secret', 'generic', ref.name, `--from-file=${ref.key}=${file}`, '--dry-run=client', '-o', 'yaml']).stdout;", - " run(['kubectl', 'apply', '--server-side', '--force-conflicts', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], secret);", - " const fetched = JSON.parse(run(['kubectl', '-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json']).stdout);", - " const raw = fetched.data?.[ref.key] || '';", - " const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);", - " results.push({ namespace: ref.namespace, secret: ref.name, key: ref.key, ok: raw.length > 0, valueBytes: decoded.length, fingerprint: raw ? 'sha256:' + crypto.createHash('sha256').update(decoded).digest('hex') : null, valuesPrinted: false });", + " const existing = tryRun(['kubectl', '-n', group.namespace, 'get', 'secret', group.name]);", + " if (existing.status !== 0) run(['kubectl', '-n', group.namespace, 'create', 'secret', 'generic', group.name]);", + " const patch = { data: {} };", + " for (const entry of group.items) patch.data[entry.key] = entry.valueBase64;", + " const patchFile = `${process.env.TMP_DIR}/${group.namespace}-${group.name}-patch.json`;", + " fs.writeFileSync(patchFile, JSON.stringify(patch));", + " run(['kubectl', '-n', group.namespace, 'patch', 'secret', group.name, '--type=merge', `--patch-file=${patchFile}`]);", + " const fetched = JSON.parse(run(['kubectl', '-n', group.namespace, 'get', 'secret', group.name, '-o', 'json']).stdout);", + " for (const entry of group.items) {", + " const raw = fetched.data?.[entry.key] || '';", + " const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);", + " results.push({ namespace: group.namespace, secret: group.name, key: entry.key, ok: raw.length > 0, valueBytes: decoded.length, fingerprint: raw ? 'sha256:' + crypto.createHash('sha256').update(decoded).digest('hex') : null, valuesPrinted: false });", + " }", "}", "console.log(JSON.stringify({ ok: results.every((item) => item.ok), secretCount: results.length, items: results, valuesPrinted: false }));", "NODE",