fix: sync agentrun v02 runner secrets

This commit is contained in:
Codex
2026-06-14 03:47:51 +00:00
parent b595b67250
commit 6e5221f0a3
3 changed files with 96 additions and 20 deletions
+9 -2
View File
@@ -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<string, unkn
secrets: spec.secrets.map((secret) => ({
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<string, unknown>, path: string): AgentRun
}
function parseLaneSecret(input: Record<string, unknown>, 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`),
};
}
+52 -18
View File
@@ -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<string, string> {
const result = new Map<string, string>();
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<string, string> {
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",