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
|
||||
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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user