fix: reload AgentRun DB secrets from YAML

This commit is contained in:
Codex
2026-06-13 07:10:20 +00:00
parent e78cc9a16d
commit 7ba28c8f40
9 changed files with 158 additions and 11 deletions
+97
View File
@@ -79,6 +79,8 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane status",
@@ -121,6 +123,7 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
if (action === "apply") return await controlPlaneApply(config, parseLaneConfirmOptions(actionArgs));
if (action === "status") return await status(config, parseStatusOptions(actionArgs));
if (action === "secret-sync") return await secretSync(config, parseSecretSyncOptions(actionArgs));
if (action === "restart") return await restartYamlLane(config, parseLaneConfirmOptions(actionArgs));
if (action === "expose") return await exposeAgentRun(config, parseConfirmOptions(actionArgs));
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(actionArgs));
if (action === "refresh") return await refresh(config, parseConfirmOptions(actionArgs));
@@ -2267,12 +2270,60 @@ async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, tar
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
restart: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
statusFull: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane} --full`,
},
valuesPrinted: false,
};
}
async function restartYamlLane(config: UniDeskConfig, options: LaneConfirmOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const plan = {
node: spec.nodeId,
kubeRoute: spec.nodeKubeRoute,
lane: spec.lane,
namespace: spec.runtime.namespace,
deployment: spec.runtime.managerDeployment,
annotation: "kubectl.kubernetes.io/restartedAt",
reason: "reload-lane-secrets-or-runtime-env",
valuesPrinted: false,
};
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane restart",
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
plan,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
const result = await capture(config, spec.nodeKubeRoute, ["script", "--", restartYamlLaneScript(spec)]);
const payload = captureJsonPayload(result);
return {
ok: result.exitCode === 0 && payload.ok !== false,
command: "agentrun control-plane restart",
mode: "confirmed-rollout-restart",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
plan,
result: payload,
capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 4000, stderrTailChars: 4000 }),
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const sources = collectLaneSecretSources(spec);
@@ -3864,6 +3915,52 @@ function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ targetRef: {
].join("\n");
}
function restartYamlLaneScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`namespace=${shQuote(spec.runtime.namespace)}`,
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
"ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"kubectl -n \"$namespace\" patch deployment \"$deployment\" --type='merge' -p \"{\\\"spec\\\":{\\\"template\\\":{\\\"metadata\\\":{\\\"annotations\\\":{\\\"kubectl.kubernetes.io/restartedAt\\\":\\\"$ts\\\"}}}}}\" >/tmp/agentrun-restart-patch.out 2>/tmp/agentrun-restart-patch.err",
"patch_rc=$?",
"if [ \"$patch_rc\" -eq 0 ]; then",
" kubectl -n \"$namespace\" rollout status deployment/\"$deployment\" --timeout=180s >/tmp/agentrun-rollout-status.out 2>/tmp/agentrun-rollout-status.err",
" rollout_rc=$?",
"else",
" rollout_rc=$patch_rc",
"fi",
"pods_json=$(kubectl -n \"$namespace\" get pod -l app.kubernetes.io/name=agentrun-mgr -o json 2>/dev/null || printf '{}')",
"deployment_json=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o json 2>/dev/null || printf '{}')",
"patch_err=$(cat /tmp/agentrun-restart-patch.err 2>/dev/null || true)",
"rollout_err=$(cat /tmp/agentrun-rollout-status.err 2>/dev/null || true)",
"PODS_JSON=\"$pods_json\" DEPLOYMENT_JSON=\"$deployment_json\" PATCH_RC=\"$patch_rc\" ROLLOUT_RC=\"$rollout_rc\" TS=\"$ts\" PATCH_ERR=\"$patch_err\" ROLLOUT_ERR=\"$rollout_err\" node <<'NODE'",
"const pods = JSON.parse(process.env.PODS_JSON || '{}');",
"const deployment = JSON.parse(process.env.DEPLOYMENT_JSON || '{}');",
"const conditions = Array.isArray(deployment.status?.conditions) ? deployment.status.conditions : [];",
"const items = Array.isArray(pods.items) ? pods.items : [];",
"const compactError = (value) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, 500) || null;",
"console.log(JSON.stringify({",
" ok: process.env.PATCH_RC === '0' && process.env.ROLLOUT_RC === '0',",
" restartedAt: process.env.TS,",
" patch: { exitCode: Number(process.env.PATCH_RC || '1'), error: compactError(process.env.PATCH_ERR) },",
" rollout: { exitCode: Number(process.env.ROLLOUT_RC || '1'), error: compactError(process.env.ROLLOUT_ERR) },",
" readyReplicas: deployment.status?.readyReplicas ?? null,",
" replicas: deployment.status?.replicas ?? null,",
" conditions: conditions.map((item) => ({ type: item.type, status: item.status, reason: item.reason ?? null })),",
" pods: items.map((pod) => ({",
" name: pod.metadata?.name ?? null,",
" phase: pod.status?.phase ?? null,",
" ready: (pod.status?.containerStatuses ?? []).map((item) => item.ready),",
" restarts: (pod.status?.containerStatuses ?? []).map((item) => item.restartCount),",
" waiting: (pod.status?.containerStatuses ?? []).map((item) => item.state?.waiting?.reason ?? null),",
" })),",
" valuesPrinted: false",
"}));",
"NODE",
"exit \"$rollout_rc\"",
].join("\n");
}
function applyYamlScript(yaml: string, fieldManager: string, dryRun: boolean): string {
const encoded = Buffer.from(yaml, "utf8").toString("base64");
return [
+1 -1
View File
@@ -63,7 +63,7 @@ export function rootHelp(): unknown {
{ command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|control-plane|git-mirror", description: "Use AgentRun v0.1 resource primitives with low-noise human output by default; session follow-up uses send only and the server decides internal steer vs turn." },
{ command: "platform-infra sub2api|langbot|n8n|wechat-archive ...", description: "Deploy platform-infra services such as Sub2API, LangBot and n8n, manage YAML-controlled public FRP/Caddy exposure and WeChat archive workflows, and inspect status/logs without printing secrets." },
{ command: "secrets plan|sync|status", description: "Plan, push and inspect YAML-declared local secret source keys to Kubernetes Secrets without printing or reverse-engineering values." },
{ command: "platform-db postgres plan|status|apply", description: "Manage YAML-declared host-native PostgreSQL 16 on PK01 for Sub2API/platform state, with PostgreSQL native TLS and redacted secret exports." },
{ command: "platform-db postgres plan|status|export-secrets|apply", description: "Manage YAML-declared host-native PostgreSQL 16 on PK01 for Sub2API/platform state, with PostgreSQL native TLS and redacted secret exports." },
{ command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." },
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
+48 -6
View File
@@ -289,11 +289,13 @@ interface RemoteJobStart {
export function platformDbHelp(): unknown {
return {
command: "platform-db postgres plan|status|apply",
command: "platform-db postgres plan|status|apply|export-secrets",
output: "json",
usage: [
`bun scripts/cli.ts platform-db postgres plan --config ${defaultConfigPath}`,
`bun scripts/cli.ts platform-db postgres status --config ${defaultConfigPath} [--full|--raw]`,
`bun scripts/cli.ts platform-db postgres export-secrets --config ${defaultConfigPath} --dry-run`,
`bun scripts/cli.ts platform-db postgres export-secrets --config ${defaultConfigPath} --confirm`,
`bun scripts/cli.ts platform-db postgres apply --config ${defaultConfigPath} --dry-run`,
`bun scripts/cli.ts platform-db postgres apply --config ${defaultConfigPath} --confirm`,
`bun scripts/cli.ts platform-db postgres apply --config ${defaultConfigPath} --confirm --wait`,
@@ -302,6 +304,7 @@ export function platformDbHelp(): unknown {
safety: {
configTruth: defaultConfigPath,
defaultMutation: false,
exportSecrets: "export-secrets only materializes YAML-declared local Secret source/export files; it does not touch the remote PostgreSQL service.",
apply: "apply --confirm returns an async UniDesk job; the job uses short trans calls and a remote PK01 job instead of keeping one long SSH session open.",
redaction: "Passwords and full DATABASE_URL values are never printed; output is limited to key names, presence, fingerprints and state.",
noK3s: "PK01 PostgreSQL is host-systemd, not Docker or k3s.",
@@ -315,6 +318,7 @@ export async function runPlatformDbCommand(config: UniDeskConfig, args: string[]
const options = parseOptions(args.slice(2));
if (action === "plan") return await plan(config, options);
if (action === "status") return await status(config, options);
if (action === "export-secrets") return await exportSecrets(options);
if (action === "apply") return await apply(config, options);
return unsupported(args);
}
@@ -359,7 +363,7 @@ function parseOptions(args: string[]): PlatformDbOptions {
throw new Error(`unsupported platform-db option: ${arg}`);
}
}
if (options.confirm && options.dryRun) throw new Error("apply accepts only one of --confirm or --dry-run");
if (options.confirm && options.dryRun) throw new Error("platform-db postgres accepts only one of --confirm or --dry-run");
return options;
}
@@ -526,6 +530,40 @@ async function apply(config: UniDeskConfig, options: PlatformDbOptions): Promise
};
}
async function exportSecrets(options: PlatformDbOptions): Promise<Record<string, unknown>> {
const pg = readPostgresHostConfig(options.configPath);
if (!options.confirm || options.dryRun) {
const localState = buildLocalSecretState(pg, false);
return {
ok: localState.inspection.ok,
action: "platform-db-postgres-export-secrets",
mode: "dry-run",
mutation: false,
config: configSummary(pg),
localSecrets: localState.summary,
next: {
confirm: `bun scripts/cli.ts platform-db postgres export-secrets --config ${pg.configPath} --confirm`,
syncConsumers: "run the consumer-specific secret sync command after confirmed export",
},
valuesPrinted: false,
};
}
const localState = ensureLocalSecretState(pg);
return {
ok: true,
action: "platform-db-postgres-export-secrets",
mode: "confirmed",
mutation: true,
config: configSummary(pg),
localSecrets: localState.summary,
next: {
status: `bun scripts/cli.ts platform-db postgres status --config ${pg.configPath}`,
syncSecrets: "run the consumer-specific secret sync command after confirmed export",
},
valuesPrinted: false,
};
}
function readPostgresHostConfig(pathArg: string): PostgresHostConfig {
const configPath = resolveConfigPath(pathArg);
const parsed = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, configPath);
@@ -1005,10 +1043,14 @@ function inspectSecrets(pg: PostgresHostConfig, materialize: boolean): SecretIns
}
function ensureLocalSecretState(pg: PostgresHostConfig): { inspection: SecretInspection; summary: Record<string, unknown>; exports: Array<Record<string, unknown>> } {
const inspection = inspectSecrets(pg, true);
return buildLocalSecretState(pg, true);
}
function buildLocalSecretState(pg: PostgresHostConfig, materialize: boolean): { inspection: SecretInspection; summary: Record<string, unknown>; exports: Array<Record<string, unknown>> } {
const inspection = inspectSecrets(pg, materialize);
if (!inspection.ok) throw new Error("required secret source keys are missing and cannot be generated from YAML");
validateObjectSecretConsistency(pg, inspection);
const exports = pg.exports.connectionStrings.map((item) => writeConnectionStringExport(pg, inspection, item));
const exports = pg.exports.connectionStrings.map((item) => connectionStringExportState(pg, inspection, item, materialize));
return {
inspection,
exports,
@@ -1035,7 +1077,7 @@ function validateObjectSecretConsistency(pg: PostgresHostConfig, inspection: Sec
}
}
function writeConnectionStringExport(pg: PostgresHostConfig, inspection: SecretInspection, item: ConnectionStringExportConfig): Record<string, unknown> {
function connectionStringExportState(pg: PostgresHostConfig, inspection: SecretInspection, item: ConnectionStringExportConfig, materialize: boolean): Record<string, unknown> {
const source = inspection.materials.get(item.sourceSecretRef);
if (source === undefined) throw new Error(`export source secret not found: ${item.sourceSecretRef}`);
const rendered = renderConnectionString(item, source.values);
@@ -1044,7 +1086,7 @@ function writeConnectionStringExport(pg: PostgresHostConfig, inspection: SecretI
const existing = existsSync(targetPath) ? parseEnvFile(readFileSync(targetPath, "utf8")) : {};
const before = existing[item.writeToSecretSource.key];
const next = { ...existing, [item.writeToSecretSource.key]: rendered };
writeEnvFile(targetPath, next);
if (materialize) writeEnvFile(targetPath, next);
return {
name: item.name,
sourceRef: item.sourceSecretRef,