From 9040d6d6f02851e7677211edd768377e8564845e Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 13 Jul 2026 22:20:44 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=20PostgreSQL=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=A7=84=E6=A8=A1=E6=8A=95=E5=BD=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/cli.md | 4 +- scripts/src/platform-db.ts | 159 ++++++++++++++++++++++++++----------- 2 files changed, 114 insertions(+), 49 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 401a1228..fbae2efa 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -116,8 +116,8 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status - `server rebuild ` 创建异步 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 [--master-server URL] [--up] [--force]` 在新计算节点生成两项配置的 provider-gateway 挂载包:`.state/provider-.env` 默认只包含 `UNIDESK_MASTER_SERVER` 与 `PROVIDER_ID`,`provider-.yml` 固定 Docker socket、`pid: "host"`、`restart: always`、只读 `/workspace` 和 SSH 维护私钥挂载;`--up` 会立即执行生成的 `docker compose up -d --build`。`provider triage [--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 host.ssh --wait-ms 15000`、`trans argv true`、`artifact-registry health --provider-id `、`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` 和 `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。 + - `plan`、`status` 和 `export-secrets` 默认返回有界摘要;配置项使用 total/passed/missing/actionable/omitted 计数,只预览固定数量的失败、缺失或待处理项,并保留 mutation、typed failure、`valuesPrinted=false` 和语义化下钻。只有显式 `--full` / `--raw` 才披露完整结构化结果。 + - `plan` 和 `status` 只读采集远端 host、PostgreSQL、TLS、DNS alias 和 Secret 形态;`export-secrets --confirm` 只按 YAML 物化本地 Secret source/export 文件,不触碰远端 PostgreSQL。export 结果用 `before` / `after` 区分写入前观察与写入后 presence,fingerprint 不披露 Secret value。 - `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 [operation args...]` / `tran [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点: diff --git a/scripts/src/platform-db.ts b/scripts/src/platform-db.ts index 219aca1e..c5da285a 100644 --- a/scripts/src/platform-db.ts +++ b/scripts/src/platform-db.ts @@ -12,6 +12,7 @@ import { compactText, fingerprintEnvValues as fingerprintValues, parseEnvFile, p const defaultConfigPath = "config/platform-db/postgres-pk01.yaml"; const managedHbaStart = "# BEGIN unidesk managed pk01-platform-postgres"; const managedHbaEnd = "# END unidesk managed pk01-platform-postgres"; +const compactActionablePreviewLimit = 4; interface PlatformDbOptions { configPath: string; @@ -1184,6 +1185,16 @@ function compactStatus( : Array.isArray(summary.cutoverBlockers) ? summary.cutoverBlockers : []; + const roles = pg.objects.roles.map((role) => ({ + name: role.name, + exists: facts?.postgres.roles.find((item) => item.name === role.name)?.exists ?? null, + })); + const databases = pg.objects.databases.map((database) => ({ + name: database.name, + owner: database.owner, + exists: facts?.postgres.databases.find((item) => item.name === database.name)?.exists ?? null, + })); + const appConnections = facts?.postgres.appConnections ?? []; return { ok, action: "platform-db-postgres-status", @@ -1233,31 +1244,37 @@ function compactStatus( 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) => connection.database) ?? [], - }, + roles: compactActionableSummary(roles, { + passed: (item) => item.exists === true, + missing: (item) => item.exists === false, + actionable: (item) => item.exists !== true, + project: (item) => item, + }), + databases: compactActionableSummary(databases, { + passed: (item) => item.exists === true, + missing: (item) => item.exists === false, + actionable: (item) => item.exists !== true, + project: (item) => item, + }), + appConnections: compactActionableSummary(appConnections, { + passed: (item) => item.ok === true && item.ssl === true, + missing: (item) => item.ok !== true || item.ssl !== true, + actionable: (item) => item.ok !== true || item.ssl !== true, + project: (item) => ({ user: item.user, database: item.database, ok: item.ok, ssl: item.ssl, error: item.error }), + }), secrets: compactSecretInspection(secrets), - exports: exports.map((item) => ({ - targetRef: item.targetRef, - key: item.key, - exists: item.exists, - present: item.present, - fingerprint: item.fingerprint, - })), + exports: compactActionableSummary(exports, { + passed: (item) => item.present === true, + missing: (item) => item.present !== true, + actionable: (item) => item.present !== true, + project: (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}`, @@ -1282,6 +1299,20 @@ function compactExportSecrets( ok: boolean, ): Record { const suffix = mode === "dry-run" ? " --dry-run" : " --confirm"; + const exports = localState.exports.map((item) => { + const before = asRecord(item.before, "export before"); + const after = asRecord(item.after, "export after"); + return { + name: item.name, + sourceRef: item.sourceRef, + targetRef: item.targetRef, + key: item.key, + before, + after, + action: item.action, + desiredFingerprint: item.desiredFingerprint, + }; + }); return { ok, action: "platform-db-postgres-export-secrets", @@ -1297,16 +1328,12 @@ function compactExportSecrets( 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, - })), + exports: compactActionableSummary(exports, { + passed: (item) => item.action === "none" && item.after.present === true, + missing: (item) => item.after.present !== true, + actionable: (item) => item.action !== "none" || item.after.present !== true, + project: (item) => item, + }), 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`, @@ -1324,23 +1351,50 @@ function compactExportSecrets( } function compactSecretInspection(secrets: SecretInspection): Record { + const entries = secrets.entries.map((entry) => ({ + sourceRef: entry.sourceRef, + exists: entry.exists, + presentKeys: entry.presentKeys, + missingKeys: entry.missingKeys, + action: entry.action, + fingerprint: entry.fingerprint, + })); 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, - })), + ...compactActionableSummary(entries, { + passed: (item) => item.exists && item.missingKeys.length === 0 && item.action === "none", + missing: (item) => !item.exists || item.missingKeys.length > 0, + actionable: (item) => item.action !== "none" || !item.exists || item.missingKeys.length > 0, + project: (item) => item, + }), valuesPrinted: false, }; } +function compactActionableSummary( + items: T[], + selectors: { + passed: (item: T) => boolean; + missing: (item: T) => boolean; + actionable: (item: T) => boolean; + project: (item: T) => unknown; + }, +): Record { + const actionableItems = items.filter(selectors.actionable); + const visibleItems = actionableItems.slice(0, compactActionablePreviewLimit); + return { + total: items.length, + passed: items.filter(selectors.passed).length, + missing: items.filter(selectors.missing).length, + actionable: actionableItems.length, + listed: visibleItems.length, + omitted: items.length - visibleItems.length, + actionableOmitted: actionableItems.length - visibleItems.length, + items: visibleItems.map(selectors.project), + }; +} + function compactRemoteFacts(facts: RemoteFacts | null): Record | null { if (facts === null) return null; return { @@ -1568,6 +1622,9 @@ function connectionStringExportState(pg: PostgresHostConfig, inspection: SecretI const exists = existsSync(targetPath); const existing = exists ? parseEnvFile(readFileSync(targetPath, "utf8")) : {}; const before = existing[item.writeToSecretSource.key]; + const beforePresent = before !== undefined && before.length > 0; + const beforeFingerprint = beforePresent ? fingerprintValues({ [item.writeToSecretSource.key]: before }, [item.writeToSecretSource.key]) : null; + const desiredFingerprint = fingerprintValues({ [item.writeToSecretSource.key]: rendered }, [item.writeToSecretSource.key]); const next = { ...existing, [item.writeToSecretSource.key]: rendered }; if (materialize) writeEnvFile(targetPath, next); return { @@ -1575,10 +1632,18 @@ function connectionStringExportState(pg: PostgresHostConfig, inspection: SecretI sourceRef: item.sourceSecretRef, targetRef: item.writeToSecretSource.sourceRef, key: item.writeToSecretSource.key, - exists, - present: before !== undefined && before.length > 0, + before: { + exists, + present: beforePresent, + fingerprint: beforeFingerprint, + }, + after: { + exists: materialize ? true : exists, + present: materialize ? true : beforePresent, + fingerprint: materialize ? desiredFingerprint : beforeFingerprint, + }, action: before === rendered ? "none" : before === undefined ? "create" : "update", - fingerprint: fingerprintValues({ [item.writeToSecretSource.key]: rendered }, [item.writeToSecretSource.key]), + desiredFingerprint, consumers: item.consumers, valuesPrinted: false, };