Merge pull request #351 from pikasTech/fix/agentrun-v02-secret-files
fix: 同步 AgentRun v02 runner Secret
This commit is contained in:
@@ -315,3 +315,38 @@ controlPlane:
|
|||||||
namespace: agentrun-v02
|
namespace: agentrun-v02
|
||||||
name: agentrun-v02-api-key
|
name: agentrun-v02-api-key
|
||||||
key: HWLAB_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
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ export interface AgentRunSecretRef {
|
|||||||
export interface AgentRunLaneSecretSpec {
|
export interface AgentRunLaneSecretSpec {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly sourceRef: string;
|
readonly sourceRef: string;
|
||||||
readonly sourceKey: string;
|
readonly sourceMode: "env" | "file";
|
||||||
|
readonly sourceKey: string | null;
|
||||||
readonly targetRef: AgentRunSecretRef;
|
readonly targetRef: AgentRunSecretRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +299,7 @@ export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unkn
|
|||||||
secrets: spec.secrets.map((secret) => ({
|
secrets: spec.secrets.map((secret) => ({
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
sourceRef: secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
|
sourceRef: secret.sourceRef.startsWith("/") ? secret.sourceRef : `.state/secrets/${secret.sourceRef}`,
|
||||||
|
sourceMode: secret.sourceMode,
|
||||||
sourceKey: secret.sourceKey,
|
sourceKey: secret.sourceKey,
|
||||||
targetRef: secret.targetRef,
|
targetRef: secret.targetRef,
|
||||||
valuesPrinted: false,
|
valuesPrinted: false,
|
||||||
@@ -511,10 +513,15 @@ function parseImageBuild(input: Record<string, unknown>, path: string): AgentRun
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseLaneSecret(input: Record<string, unknown>, path: string): AgentRunLaneSecretSpec {
|
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 {
|
return {
|
||||||
id: stringField(input, "id", path),
|
id: stringField(input, "id", path),
|
||||||
sourceRef: secretSourceRefField(input, "sourceRef", path),
|
sourceRef: secretSourceRefField(input, "sourceRef", path),
|
||||||
sourceKey: stringField(input, "sourceKey", path),
|
sourceMode,
|
||||||
|
sourceKey,
|
||||||
targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
|
targetRef: parseNamespacedSecretRef(recordField(input, "targetRef", path), `${path}.targetRef`),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-18
@@ -2339,13 +2339,14 @@ async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Pr
|
|||||||
valuesPrinted: false,
|
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 }) => ({
|
const plan = values.map(({ spec: item, value }) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
namespace: item.targetRef.namespace,
|
namespace: item.targetRef.namespace,
|
||||||
secret: item.targetRef.name,
|
secret: item.targetRef.name,
|
||||||
key: item.targetRef.key,
|
key: item.targetRef.key,
|
||||||
sourceRef: item.sourceRef,
|
sourceRef: item.sourceRef,
|
||||||
|
sourceMode: item.sourceMode,
|
||||||
sourceKey: item.sourceKey,
|
sourceKey: item.sourceKey,
|
||||||
sourcePath: value.redactedPath,
|
sourcePath: value.redactedPath,
|
||||||
fingerprint: value.fingerprint,
|
fingerprint: value.fingerprint,
|
||||||
@@ -3758,15 +3759,32 @@ function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
|
|||||||
].join("\n");
|
].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}`);
|
if (sourceRef.includes("..")) throw new Error(`secret sourceRef must not contain ..: ${sourceRef}`);
|
||||||
const sourcePath = sourceRef.startsWith("/")
|
const sourcePath = sourceRef.startsWith("/")
|
||||||
? sourceRef
|
? sourceRef
|
||||||
: join(resolveSecretSourceRoot(spec), ...sourceRef.split("/"));
|
: join(resolveSecretSourceRoot(spec), ...sourceRef.split("/"));
|
||||||
if (!existsSync(sourcePath)) throw new Error(`secret source ${sourceRef} is missing`);
|
if (!existsSync(sourcePath)) throw new Error(`secret source ${sourceRef} is missing`);
|
||||||
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
|
let value: string;
|
||||||
const value = values.get(key);
|
if (source.sourceMode === "file") {
|
||||||
if (value === undefined || value.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${key}`);
|
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 {
|
return {
|
||||||
redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
|
redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
|
||||||
value,
|
value,
|
||||||
@@ -3793,8 +3811,9 @@ function resolveSecretSourceRoot(spec: AgentRunLaneSpec): string {
|
|||||||
function parseEnvFile(text: string): Map<string, string> {
|
function parseEnvFile(text: string): Map<string, string> {
|
||||||
const result = new Map<string, string>();
|
const result = new Map<string, string>();
|
||||||
for (const rawLine of text.split(/\r?\n/u)) {
|
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.length === 0 || line.startsWith("#")) continue;
|
||||||
|
if (line.startsWith("export ")) line = line.slice("export ".length).trim();
|
||||||
const index = line.indexOf("=");
|
const index = line.indexOf("=");
|
||||||
if (index <= 0) continue;
|
if (index <= 0) continue;
|
||||||
const key = line.slice(0, index).trim();
|
const key = line.slice(0, index).trim();
|
||||||
@@ -3807,12 +3826,13 @@ function parseEnvFile(text: string): Map<string, string> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectLaneSecretSources(spec: AgentRunLaneSpec): Array<{ id: string; sourceRef: string; sourceKey: string; targetRef: { namespace: string; name: string; key: string } }> {
|
function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSource[] {
|
||||||
const result: Array<{ id: string; sourceRef: string; sourceKey: string; targetRef: { namespace: string; name: string; key: string } }> = [];
|
const result: LaneSecretSource[] = [];
|
||||||
if (spec.database.secretSourceRef !== null) {
|
if (spec.database.secretSourceRef !== null) {
|
||||||
result.push({
|
result.push({
|
||||||
id: "database",
|
id: "database",
|
||||||
sourceRef: spec.database.secretSourceRef,
|
sourceRef: spec.database.secretSourceRef,
|
||||||
|
sourceMode: "env",
|
||||||
sourceKey: spec.database.secretRef.key,
|
sourceKey: spec.database.secretRef.key,
|
||||||
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: 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({
|
result.push({
|
||||||
id: secret.id,
|
id: secret.id,
|
||||||
sourceRef: secret.sourceRef,
|
sourceRef: secret.sourceRef,
|
||||||
|
sourceMode: secret.sourceMode,
|
||||||
sourceKey: secret.sourceKey,
|
sourceKey: secret.sourceKey,
|
||||||
targetRef: secret.targetRef,
|
targetRef: secret.targetRef,
|
||||||
});
|
});
|
||||||
@@ -3852,25 +3873,38 @@ function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ targetRef: {
|
|||||||
"const crypto = require('node:crypto');",
|
"const crypto = require('node:crypto');",
|
||||||
"const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));",
|
"const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));",
|
||||||
"const results = [];",
|
"const results = [];",
|
||||||
|
"const groups = new Map();",
|
||||||
"function run(argv, input) {",
|
"function run(argv, input) {",
|
||||||
" const out = cp.spawnSync(argv[0], argv.slice(1), { input, encoding: 'utf8' });",
|
" 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}`);",
|
" if (out.status !== 0) throw new Error(`${argv.join(' ')} failed: ${out.stderr || out.stdout}`);",
|
||||||
" return out;",
|
" return out;",
|
||||||
"}",
|
"}",
|
||||||
|
"function tryRun(argv) {",
|
||||||
|
" return cp.spawnSync(argv[0], argv.slice(1), { encoding: 'utf8' });",
|
||||||
|
"}",
|
||||||
"for (let index = 0; index < items.length; index += 1) {",
|
"for (let index = 0; index < items.length; index += 1) {",
|
||||||
" const item = items[index];",
|
" const item = items[index];",
|
||||||
" const ref = item.targetRef;",
|
" const ref = item.targetRef;",
|
||||||
" const value = Buffer.from(item.valueBase64, 'base64');",
|
" const groupKey = `${ref.namespace}\\u0000${ref.name}`;",
|
||||||
" const file = `${process.env.TMP_DIR}/secret-${index}`;",
|
" if (!groups.has(groupKey)) groups.set(groupKey, { namespace: ref.namespace, name: ref.name, items: [] });",
|
||||||
" fs.writeFileSync(file, value);",
|
" groups.get(groupKey).items.push({ key: ref.key, valueBase64: item.valueBase64 });",
|
||||||
" const ns = run(['kubectl', 'create', 'namespace', ref.namespace, '--dry-run=client', '-o', 'yaml']).stdout;",
|
"}",
|
||||||
|
"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);",
|
" 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;",
|
" const existing = tryRun(['kubectl', '-n', group.namespace, 'get', 'secret', group.name]);",
|
||||||
" run(['kubectl', 'apply', '--server-side', '--force-conflicts', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], secret);",
|
" if (existing.status !== 0) run(['kubectl', '-n', group.namespace, 'create', 'secret', 'generic', group.name]);",
|
||||||
" const fetched = JSON.parse(run(['kubectl', '-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json']).stdout);",
|
" const patch = { data: {} };",
|
||||||
" const raw = fetched.data?.[ref.key] || '';",
|
" for (const entry of group.items) patch.data[entry.key] = entry.valueBase64;",
|
||||||
" const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);",
|
" const patchFile = `${process.env.TMP_DIR}/${group.namespace}-${group.name}-patch.json`;",
|
||||||
" 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 });",
|
" 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 }));",
|
"console.log(JSON.stringify({ ok: results.every((item) => item.ok), secretCount: results.length, items: results, valuesPrinted: false }));",
|
||||||
"NODE",
|
"NODE",
|
||||||
|
|||||||
Reference in New Issue
Block a user