fix: 收敛 PostgreSQL 状态与 Secret 导出输出

This commit is contained in:
Codex
2026-07-13 22:07:26 +02:00
parent 240c19391a
commit 740957570a
2 changed files with 217 additions and 7 deletions
+212 -6
View File
@@ -304,8 +304,8 @@ export function platformDbHelp(): unknown {
usage: [
`bun scripts/cli.ts platform-db postgres plan --config ${defaultConfigPath} [--full|--raw]`,
`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 export-secrets --config ${defaultConfigPath} --dry-run [--full|--raw]`,
`bun scripts/cli.ts platform-db postgres export-secrets --config ${defaultConfigPath} --confirm [--full|--raw]`,
`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`,
@@ -318,6 +318,7 @@ export function platformDbHelp(): unknown {
configTruth: defaultConfigPath,
defaultMutation: false,
exportSecrets: "export-secrets only materializes YAML-declared local Secret source/export files; it does not touch the remote PostgreSQL service.",
disclosure: "plan, status and export-secrets use bounded summaries by default; --full and --raw explicitly disclose the complete structured result without printing Secret values.",
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.",
monitorReset: "monitor reset is restricted by owning YAML to the dedicated web_probe_monitor database and requires --confirm; it cannot accept a database name or arbitrary SQL.",
redaction: "Passwords and full DATABASE_URL values are never printed; output is limited to key names, presence, fingerprints and state.",
@@ -488,7 +489,8 @@ async function status(config: UniDeskConfig, options: PlatformDbOptions): Promis
...(secrets.ok ? [] : ["secrets-unhealthy"]),
...(endpointHealthy ? [] : [controllerConnection.attempted ? "connection-host-probe-failed" : "connection-host-probe-unavailable"]),
];
return {
const exportTargets = inspectExportTargets(pg);
const result = {
ok: remote.capture.exitCode === 0 && facts !== null && deploymentHealthy && secrets.ok,
action: "platform-db-postgres-status",
mutation: false,
@@ -525,9 +527,12 @@ async function status(config: UniDeskConfig, options: PlatformDbOptions): Promis
},
remoteFacts: facts,
secrets: secretSummary(secrets),
exports: exportTargets,
remote: compactCapture(remote.capture, { full: options.full || remote.capture.exitCode !== 0 }),
...(options.raw ? { raw: remote.capture } : {}),
};
if (options.full) return result;
return compactStatus(pg, secrets, exportTargets, facts, controllerConnection, remote.capture, result.summary, result.ok);
}
async function apply(config: UniDeskConfig, options: PlatformDbOptions): Promise<Record<string, unknown>> {
@@ -603,7 +608,7 @@ async function exportSecrets(options: PlatformDbOptions): Promise<Record<string,
const pg = readPostgresHostConfig(options.configPath);
if (!options.confirm || options.dryRun) {
const localState = buildLocalSecretState(pg, false);
return {
const result = {
ok: localState.inspection.ok,
action: "platform-db-postgres-export-secrets",
mode: "dry-run",
@@ -616,9 +621,11 @@ async function exportSecrets(options: PlatformDbOptions): Promise<Record<string,
},
valuesPrinted: false,
};
if (options.full) return result;
return compactExportSecrets(pg, localState, "dry-run", false, result.ok);
}
const localState = ensureLocalSecretState(pg);
return {
const result = {
ok: true,
action: "platform-db-postgres-export-secrets",
mode: "confirmed",
@@ -631,6 +638,8 @@ async function exportSecrets(options: PlatformDbOptions): Promise<Record<string,
},
valuesPrinted: false,
};
if (options.full) return result;
return compactExportSecrets(pg, localState, "confirmed", true, result.ok);
}
function readPostgresHostConfig(pathArg: string): PostgresHostConfig {
@@ -1160,6 +1169,178 @@ function compactPlan(
};
}
function compactStatus(
pg: PostgresHostConfig,
secrets: SecretInspection,
exports: Array<Record<string, unknown>>,
facts: RemoteFacts | null,
controllerConnection: ControllerConnectionProbe,
capture: SshCaptureResult,
summary: Record<string, unknown> | null,
ok: boolean,
): Record<string, unknown> {
const blockers = summary === null
? ["remote-facts-unavailable"]
: Array.isArray(summary.cutoverBlockers)
? summary.cutoverBlockers
: [];
return {
ok,
action: "platform-db-postgres-status",
mutation: false,
failure: ok ? null : {
type: capture.exitCode !== 0 || facts === null
? "remote-facts-unavailable"
: secrets.ok
? "deployment-unhealthy"
: "secrets-unhealthy",
blockers,
},
target: {
node: pg.node.id,
route: pg.node.route,
mode: pg.node.mode,
},
config: {
path: pg.configPath,
id: pg.metadata.id,
pgVersion: pg.postgres.package.version,
connectionHost: pg.postgres.network.connectionHost,
port: pg.postgres.network.port,
sslmode: pg.postgres.network.sslmode,
},
summary: summary === null ? null : {
healthy: summary.healthy,
deploymentHealthy: summary.deploymentHealthy,
cutoverReady: summary.cutoverReady,
cutoverBlockers: summary.cutoverBlockers,
packageInstalled: summary.packageInstalled,
serviceActive: summary.serviceActive,
serviceEnabled: summary.serviceEnabled,
sslOn: summary.sslOn,
port5432Listening: summary.port5432Listening,
dnsResolves: summary.dnsResolves,
dnsDisposition: summary.dnsDisposition,
secretsOk: summary.secretsOk,
connectionHostProbe: {
attempted: controllerConnection.attempted,
ok: controllerConnection.ok,
ssl: controllerConnection.ssl,
user: controllerConnection.user,
database: controllerConnection.database,
host: controllerConnection.host,
port: controllerConnection.port,
error: controllerConnection.error,
},
},
roles: pg.objects.roles.map((role) => ({
name: role.name,
exists: facts?.postgres.roles.find((item) => item.name === role.name)?.exists ?? null,
})),
databases: pg.objects.databases.map((database) => ({
name: database.name,
owner: database.owner,
exists: facts?.postgres.databases.find((item) => item.name === database.name)?.exists ?? null,
})),
appConnections: {
ok: facts?.postgres.appConnections.every((connection) => connection.ok === true && connection.ssl === true) ?? false,
passed: facts?.postgres.appConnections.filter((connection) => connection.ok === true && connection.ssl === true).length ?? 0,
total: facts?.postgres.appConnections.length ?? 0,
failed: facts?.postgres.appConnections
.filter((connection) => connection.ok !== true || connection.ssl !== true)
.map((connection) => ({ user: connection.user, database: connection.database, error: connection.error })) ?? [],
},
secrets: compactSecretInspection(secrets),
exports: exports.map((item) => ({
targetRef: item.targetRef,
key: item.key,
exists: item.exists,
present: item.present,
fingerprint: item.fingerprint,
})),
remote: compactPlanCapture(capture),
next: {
plan: `bun scripts/cli.ts platform-db postgres plan --config ${pg.configPath}`,
full: `bun scripts/cli.ts platform-db postgres status --config ${pg.configPath} --full`,
raw: `bun scripts/cli.ts platform-db postgres status --config ${pg.configPath} --raw`,
},
disclosure: {
compact: true,
omitted: ["host facts", "package repository", "TLS paths", "raw remote capture"],
fullAvailable: true,
rawAvailable: true,
},
valuesPrinted: false,
};
}
function compactExportSecrets(
pg: PostgresHostConfig,
localState: { inspection: SecretInspection; summary: Record<string, unknown>; exports: Array<Record<string, unknown>> },
mode: "dry-run" | "confirmed",
mutation: boolean,
ok: boolean,
): Record<string, unknown> {
const suffix = mode === "dry-run" ? " --dry-run" : " --confirm";
return {
ok,
action: "platform-db-postgres-export-secrets",
mode,
mutation,
failure: ok ? null : {
type: "secrets-unhealthy",
blockers: localState.inspection.entries.filter((entry) => entry.missingKeys.length > 0).map((entry) => entry.sourceRef),
},
config: {
path: pg.configPath,
id: pg.metadata.id,
secretRoot: localState.inspection.root,
},
secrets: compactSecretInspection(localState.inspection),
exports: localState.exports.map((item) => ({
name: item.name,
sourceRef: item.sourceRef,
targetRef: item.targetRef,
key: item.key,
exists: item.exists,
present: item.present,
action: item.action,
fingerprint: item.fingerprint,
})),
next: {
...(mode === "dry-run" ? { confirm: `bun scripts/cli.ts platform-db postgres export-secrets --config ${pg.configPath} --confirm` } : {}),
full: `bun scripts/cli.ts platform-db postgres export-secrets --config ${pg.configPath}${suffix} --full`,
raw: `bun scripts/cli.ts platform-db postgres export-secrets --config ${pg.configPath}${suffix} --raw`,
syncConsumers: "run the consumer-specific secret sync command after confirmed export",
},
disclosure: {
compact: true,
omitted: ["source paths", "required key lists", "consumer Secret details"],
fullAvailable: true,
rawAvailable: true,
},
valuesPrinted: false,
};
}
function compactSecretInspection(secrets: SecretInspection): Record<string, unknown> {
return {
ok: secrets.ok,
root: secrets.root,
entries: secrets.entries.map((entry) => ({
sourceRef: entry.sourceRef,
exists: entry.exists,
keys: {
present: entry.presentKeys,
missing: entry.missingKeys,
},
action: entry.action,
fingerprint: entry.fingerprint,
})),
valuesPrinted: false,
};
}
function compactRemoteFacts(facts: RemoteFacts | null): Record<string, unknown> | null {
if (facts === null) return null;
return {
@@ -1384,7 +1565,8 @@ function connectionStringExportState(pg: PostgresHostConfig, inspection: SecretI
const rendered = renderConnectionString(item, source.values);
const root = secretRoot(pg);
const targetPath = join(root, item.writeToSecretSource.sourceRef);
const existing = existsSync(targetPath) ? parseEnvFile(readFileSync(targetPath, "utf8")) : {};
const exists = existsSync(targetPath);
const existing = exists ? parseEnvFile(readFileSync(targetPath, "utf8")) : {};
const before = existing[item.writeToSecretSource.key];
const next = { ...existing, [item.writeToSecretSource.key]: rendered };
if (materialize) writeEnvFile(targetPath, next);
@@ -1393,6 +1575,8 @@ function connectionStringExportState(pg: PostgresHostConfig, inspection: SecretI
sourceRef: item.sourceSecretRef,
targetRef: item.writeToSecretSource.sourceRef,
key: item.writeToSecretSource.key,
exists,
present: before !== undefined && before.length > 0,
action: before === rendered ? "none" : before === undefined ? "create" : "update",
fingerprint: fingerprintValues({ [item.writeToSecretSource.key]: rendered }, [item.writeToSecretSource.key]),
consumers: item.consumers,
@@ -1400,6 +1584,28 @@ function connectionStringExportState(pg: PostgresHostConfig, inspection: SecretI
};
}
function inspectExportTargets(pg: PostgresHostConfig): Array<Record<string, unknown>> {
const root = secretRoot(pg);
return pg.exports.connectionStrings.map((item) => {
const targetPath = join(root, item.writeToSecretSource.sourceRef);
const exists = existsSync(targetPath);
const values = exists ? parseEnvFile(readFileSync(targetPath, "utf8")) : {};
const value = values[item.writeToSecretSource.key];
const present = value !== undefined && value.length > 0;
return {
name: item.name,
sourceRef: item.sourceSecretRef,
targetRef: item.writeToSecretSource.sourceRef,
key: item.writeToSecretSource.key,
exists,
present,
fingerprint: present ? fingerprintValues({ [item.writeToSecretSource.key]: value }, [item.writeToSecretSource.key]) : null,
consumerScopes: item.consumers.map((consumer) => consumer.scope),
valuesPrinted: false,
};
});
}
function renderConnectionString(item: ConnectionStringExportConfig, values: Record<string, string>): string {
let output = item.render.format;
const merged = { ...values, ...(item.render.variables ?? {}) };