fix: 收敛 PostgreSQL 配置规模投影

This commit is contained in:
Codex
2026-07-13 22:20:44 +02:00
parent 8d1cd67597
commit 9040d6d6f0
2 changed files with 114 additions and 49 deletions
+2 -2
View File
@@ -116,8 +116,8 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status
- `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``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` 区分写入前观察与写入后 presencefingerprint 不披露 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 公网 endpointDNS alias 不作为 `sslmode=require` 切库 blockerPK01 规则见 `docs/reference/pk01.md`
- `trans <route> [operation args...]` / `tran <route> [operation args...]` 通过 backend-core 内网 WebSocket broker 和 provider-gateway 的 Host SSH / WSL SSH 维护桥连接目标节点:
+112 -47
View File
@@ -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<string, unknown> {
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<string, unknown> {
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<T>(
items: T[],
selectors: {
passed: (item: T) => boolean;
missing: (item: T) => boolean;
actionable: (item: T) => boolean;
project: (item: T) => unknown;
},
): Record<string, unknown> {
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<string, unknown> | 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,
};