fix: 收敛 PostgreSQL 状态与 Secret 导出输出
This commit is contained in:
@@ -115,7 +115,11 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status
|
||||
- `gc plan|run --confirm|db-trace|policy|remote` 是主 server 和受控 provider 的磁盘高水位一次性缓解与长期防膨胀入口。`plan` 只读输出候选、风险、估算收益和保护对象;`run` 必须显式 `--confirm`;`gc remote <providerId> ...` 通过 UniDesk SSH 透传执行远端 GC,`--target-use-percent N` 会在 `summary.target` 中报告目标水位所需释放量、候选估算、预计水位、缺口和 safe-stop 决策。默认只包含 allowlisted `/tmp` 诊断目录;非 allowlist stale `/tmp` 直接子项必须显式 `--include-stale-tmp`,并只允许删除 `/tmp` 一级子项且避开系统 socket/session 前缀。G14/HWLAB registry retention、受限 core dump、保护对象、safe-stop 线和长期收益表的权威规则见 `docs/reference/gc.md`。
|
||||
- `server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>` 创建异步 job,先构建目标服务镜像,随后在 `.state/locks/server-compose.lock` 串行保护下用 `--no-deps --force-recreate` 替换目标 service 并等待容器 `healthy/running`。Todo Note 的 CC01/旧 Compose 回退入口已在迁移验收后删除;正式发布只走 NC01 PaC/Argo。D601 Code Queue 执行面不由 `server rebuild` 管理;Rust backend-core 常规迭代不得用该命令在 master server 编译,只有明确的 backend-core 主 server 上线例外可以按限流、异步轮询和 health 证据执行,规则见 `docs/reference/dev-environment.md`。
|
||||
- `provider attach <providerId> [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-<ID>.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-<ID>.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。`provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]` 是只读多信号健康裁决入口,会把单路径 `provider is not online`、SSH 超时、registry 失败和 service proxy 失败归类成 `runner-local-observation-gap`、`service-degraded`、`provider-degraded` 或 `global-blocker`。默认输出只返回裁决、scope、失败/降级/未知信号和有界 evidence 摘要,完整 evidence 必须显式加 `--full` 或 `--raw`;推荐交叉验证命令仍包含 `debug health`、`debug dispatch <providerId> host.ssh --wait-ms 15000`、`trans <providerId> argv true`、`artifact-registry health --provider-id <providerId>`、`microservice health k3sctl-adapter`、`microservice health code-queue` 和 `codex tasks --view supervisor --limit 20`。
|
||||
- `platform-db postgres plan|status|export-secrets|apply --config config/platform-db/postgres-pk01.yaml` 管理 YAML 声明的 PK01 host-native PostgreSQL。`plan` 和 `status` 只读采集远端 host、PostgreSQL、TLS、DNS alias 和 Secret 形态;`export-secrets --confirm` 只按 YAML 物化本地 Secret source/export 文件,不触碰远端 PostgreSQL;`apply --confirm` 默认创建本地异步 job,`apply --confirm --wait` 用远端 root-owned job 收敛 systemd PostgreSQL、TLS、`pg_hba`、role/database、Secret export 和备份 timer。输出不得打印密码或完整 `DATABASE_URL`。跨节点消费者使用 YAML 中的 `connectionHost` 直连 PK01 公网 endpoint,DNS alias 不作为 `sslmode=require` 切库 blocker,PK01 规则见 `docs/reference/pk01.md`。
|
||||
- `platform-db postgres plan|status|export-secrets|apply --config config/platform-db/postgres-pk01.yaml` 管理 YAML 声明的 PK01 host-native PostgreSQL:
|
||||
- `plan`、`status` 和 `export-secrets` 默认返回有界摘要,保留 role/database、Secret/export key 的 presence、fingerprint、mutation、typed failure、`valuesPrinted=false` 和语义化下钻;只有显式 `--full` / `--raw` 才披露完整结构化结果。
|
||||
- `plan` 和 `status` 只读采集远端 host、PostgreSQL、TLS、DNS alias 和 Secret 形态;`export-secrets --confirm` 只按 YAML 物化本地 Secret source/export 文件,不触碰远端 PostgreSQL。
|
||||
- `apply --confirm` 默认创建本地异步 job;`apply --confirm --wait` 用远端 root-owned job 收敛 systemd PostgreSQL、TLS、`pg_hba`、role/database、Secret export 和备份 timer。
|
||||
- 输出不得打印密码或完整 `DATABASE_URL`;跨节点消费者使用 YAML 中的 `connectionHost` 直连 PK01 公网 endpoint,DNS alias 不作为 `sslmode=require` 切库 blocker,PK01 规则见 `docs/reference/pk01.md`。
|
||||
- `trans <route> [operation args...]` / `tran <route> [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点:
|
||||
- `route` 基础形态是 provider id,例如 `D601` 或 `G14`;也可以扩展为纯定位路径 `provider:plane[:namespace:resource[:container]]`,例如 `D601:win`、`D601:win/c/test`、`G14:k3s`、`D601:k3s` 或 `G14:k3s:<namespace>:<workload>`。
|
||||
- WSL provider 的 Windows plane 固定使用 `win`,不得使用 `win32`;Windows route 中 `win` 只负责定位,operation 直接写 `ps`、`cmd`、`git` 或 fs helper。
|
||||
|
||||
+212
-6
@@ -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 ?? {}) };
|
||||
|
||||
Reference in New Issue
Block a user