Merge pull request #2073 from pikasTech/fix/2061-pikaoa-test-search-path
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

修复 PikaOA 测试数据库 search_path
This commit is contained in:
Lyon
2026-07-15 03:05:55 +08:00
committed by GitHub
6 changed files with 118 additions and 2 deletions
+1
View File
@@ -40,6 +40,7 @@ testRuntime:
image: postgres:16-alpine
name: pikaoa_test
username: pikaoa_test
schema: pikaoa
port: 5432
password:
sourceRef: ./secrets/pikaoa-test-database-password.txt
@@ -0,0 +1,36 @@
# R1.5.9.2 任务报告
## 交付
- Issue[pikasTech/unidesk#2061](https://github.com/pikasTech/unidesk/issues/2061)。
- PR[pikasTech/unidesk#2073](https://github.com/pikasTech/unidesk/pull/2073)。
- 实现提交:[`bc6285a8`](https://github.com/pikasTech/unidesk/commit/bc6285a892e44166afb2fc5899836fe054163c15)。
- 目标分支:`fix/2061-pikaoa-test-search-path`
## 结果
- `testRuntime.targets.*.database.schema` 成为 owning YAML 必填字段,并按简单 PostgreSQL identifier 校验。
- runtime DSN 通过 URL API 结构化写入 `sslmode=disable` 和 YAML schema 对应的 `search_path`,不提供隐藏默认值。
- fixture 保持 `validationOnly=true`,计划投影只披露 schema、DSN 参数 presence 和共享 runtime config 挂载关系。
- 迁移 Job、API 和 Worker 继续挂载同一 Secret key `pikaoa.yaml`,使用相同数据库 schema。
- 缺少 schema 时在任何远端调用前返回具名配置错误。
## 验证
- `bun --check scripts/src/pikaoa-test-target.ts` 通过。
- `bun test scripts/src/pikaoa-test-target-async.test.ts` 通过,共 `2 pass`
- fixture plan 结构断言通过:
- `schema=pikaoa`
- `sslmode``search_path` presence 均为 `true`
- runtime config 消费者为 migration、API 和 Worker
- `validationOnly=true``remoteQueried=false`
- 脱敏断言通过:
- 顶层和数据库、runtime config 投影均保持 `valuesPrinted=false`
- 输出不含完整 DSN、fixture 数据库密码或其他 Secret 值。
- `git diff --check` 通过。
## 边界与风险
- 未连接真实集群,未修改正式 PK01、Secret、真实 target 或生产运行面。
- 未部署、未合并;PR 等待主代理 post-task 审阅与后续处置。
- 本次只验证结构化渲染、fixture 和脱敏契约;真实专用测试 target 仍需后续由用户明确声明并按原入口验收。
@@ -95,6 +95,9 @@
##### R1.5.9.1 [completed]
`pikaoa test-target start` 的 PostgreSQL、迁移和 rollout 长等待改为提交即返回与 `status` 短轮询,保持真实失败可见和 Secret 脱敏,执行记录见 [pikasTech/unidesk#2060](https://github.com/pikasTech/unidesk/issues/2060),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.1_Task_Report.md)。
##### R1.5.9.2 [completed]
修复 PikaOA 临时测试 target 的数据库 schema/search_path,使迁移 Job、API 和 Worker 通过 YAML 声明的同一 schema 访问业务表,保持 DSN 与 Secret 脱敏,执行记录见 [pikasTech/unidesk#2061](https://github.com/pikasTech/unidesk/issues/2061),完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.5.9.2_Task_Report.md)。
### R1.6
审核并合并双仓 PR,执行首次受控 PaC bootstrap,由正常 source PR merge 自动发布,依赖 R1.3-R1.5,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R1.6_Task_Report.md)。
+6 -1
View File
@@ -50,6 +50,10 @@ bun scripts/cli.ts pikaoa test-target stop --target <id> --instance <run-id> --c
- `delivery.targets.*.ci.namespace`
- 测试入口不得渲染或删除保护集合中的任何对象。
- PostgreSQL 使用 namespace 内的临时存储,namespace 删除后测试数据随之清理。
- 临时数据库连接只由 target YAML 渲染:
- `database.name``database.username``database.schema` 都是必填 PostgreSQL identifier
- renderer 使用结构化 URL 参数写入 `sslmode``search_path`,不提供隐藏 schema 默认值;
- 缺少 schema 或形状不合法时,在任何 route 调用前返回具名配置错误。
- 附件运行参数与 namespace 内的临时 PVC 来自 target YAML
- `runtime.attachment.storageRoot``runtime.attachment.maxBytes` 直接写入严格 runtime YAML
- PVC 名称、容量、StorageClass 和 access mode 只从同一 target 声明读取;
@@ -69,10 +73,11 @@ bun scripts/cli.ts pikaoa test-target stop --target <id> --instance <run-id> --c
- Secret 值只从 YAML `sourceRef` 读取并写入目标 Secret。
- CLI 输出只显示 `sourceRef``targetKey`、presence 和 fingerprint,始终保持 `valuesPrinted=false`
- `plan` 的数据库投影只显示 schema 和 DSN 参数 presence,不显示完整 DSN、用户名、密码或参数值。
- 含 Secret 的 runner 文件使用 owner-only 权限,运行后立即删除;manifest 只在 runner 的临时目录存活,终态前由 trap 清理。
- 状态和事件只记录公开 ID、阶段、typed code 和时间,不保存命令 stderr、DSN、manifest 正文或 Secret 值。
- Secret 缺失会在连接测试集群前阻塞 `start`
- API 和 Worker 从同一 Secret 挂载严格的 PikaOA runtime YAML。
- 迁移 Job、API 和 Worker 从同一 Secret key 挂载严格的 PikaOA runtime YAML,数据库 schema 与 `search_path` 因此保持一致
- OTel endpoint、Prometheus scrape、指标路径和探针全部由 target YAML 声明。
- 配置一致性、镜像/commit 漂移和 OTel exporter 失败只产生 `blocking=false` warning,不作为 MVP 门禁。
@@ -239,6 +239,44 @@ test("validation-only fixture never invokes remote capture", async () => {
assert.equal(payload.remoteQueried, false);
assert.equal((payload.status as Record<string, unknown>).remoteQueried, false);
const planResult = await runPikaoaCommand({} as UniDeskConfig, [
"test-target", "plan", "--config", "config/fixtures/pikaoa-test-target.yaml", "--target", "TEST01", "--instance", "validation-only", "--commit", "0123456789abcdef", "--output", "json",
], forbiddenCapture);
const planPayload = JSON.parse(planResult.renderedText) as Record<string, unknown>;
const plan = planPayload.plan as Record<string, unknown>;
assert.deepEqual(plan.database, {
schema: "pikaoa",
dsnParameterPresence: { sslmode: true, search_path: true },
valuesPrinted: false,
});
assert.deepEqual(plan.runtimeConfig, {
secretName: "pikaoa-test-runtime",
key: "pikaoa.yaml",
mountPath: "/etc/pikaoa/pikaoa.yaml",
consumers: ["migration", "api", "worker"],
valuesPrinted: false,
});
assert.equal(planPayload.valuesPrinted, false);
assert.equal(JSON.stringify(planPayload).includes("fixture-db-password"), false);
assert.equal(JSON.stringify(planPayload).includes("postgres://"), false);
const missingSchemaRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-schema-"));
const missingSchemaPath = join(missingSchemaRoot, "pikaoa.yaml");
const missingSchemaFixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8")
.replace(" schema: pikaoa\n", "");
writeFileSync(missingSchemaPath, missingSchemaFixture);
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, [
"test-target", "status", "--config", missingSchemaPath, "--target", "TEST01", "--instance", "missing-schema", "--output", "json",
], forbiddenCapture),
/testRuntime\.targets\.TEST01\.database\.schema /u,
);
assert.equal(calls, 0);
} finally {
rmSync(missingSchemaRoot, { recursive: true, force: true });
}
const unconfigured = await runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--output", "json"], forbiddenCapture);
const unconfiguredPayload = JSON.parse(unconfigured.renderedText) as Record<string, unknown>;
assert.equal(calls, 0);
+34 -1
View File
@@ -61,6 +61,7 @@ interface TestTargetSpec {
image: string;
name: string;
username: string;
schema: string;
port: number;
password: SecretSourceSpec;
};
@@ -319,6 +320,7 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
image: imageReference(database.image, `${path}.database.image`),
name: postgresIdentifier(database.name, `${path}.database.name`),
username: postgresIdentifier(database.username, `${path}.database.username`),
schema: postgresIdentifier(database.schema, `${path}.database.schema`),
port: positiveInteger(database.port, `${path}.database.port`, 65_535),
password: secretSource(database.password, "database.password", `${path}.database.password`),
},
@@ -447,6 +449,14 @@ function renderedPlan(selection: Selection, context: RenderContext): Record<stri
valuesPrinted: false,
sources: secretSources(target).map((source) => ({ ...source, presence: "unchecked", fingerprint: null, valuesPrinted: false })),
},
database: databasePlanSummary(target),
runtimeConfig: {
secretName: target.runtime.secretName,
key: "pikaoa.yaml",
mountPath: "/etc/pikaoa/pikaoa.yaml",
consumers: ["migration", "api", "worker"],
valuesPrinted: false,
},
probes: {
api: { liveness: target.probes.apiLivenessPath, readiness: target.probes.apiReadinessPath, metrics: target.observability.apiMetricsPath },
worker: { liveness: target.probes.workerLivenessPath, readiness: target.probes.workerReadinessPath, metrics: target.observability.workerMetricsPath },
@@ -741,7 +751,7 @@ function deployment(
}
function runtimeSecretData(target: TestTargetSpec, context: RenderContext, secrets: SecretMaterial): Record<string, string> {
const databaseURL = `postgres://${encodeURIComponent(target.database.username)}:${encodeURIComponent(secrets.databasePassword)}@pikaoa-postgres:${target.database.port}/${encodeURIComponent(target.database.name)}?sslmode=disable`;
const databaseURL = renderDatabaseURL(target, secrets.databasePassword);
return {
[target.database.password.targetKey]: secrets.databasePassword,
[target.runtime.sessionSecret.targetKey]: secrets.sessionSecret,
@@ -773,6 +783,29 @@ function runtimeSecretData(target: TestTargetSpec, context: RenderContext, secre
};
}
function renderDatabaseURL(target: TestTargetSpec, password: string): string {
const databaseURL = new URL("postgres://pikaoa-postgres");
databaseURL.username = target.database.username;
databaseURL.password = password;
databaseURL.port = String(target.database.port);
databaseURL.pathname = `/${target.database.name}`;
databaseURL.searchParams.set("sslmode", "disable");
databaseURL.searchParams.set("search_path", target.database.schema);
return databaseURL.toString();
}
function databasePlanSummary(target: TestTargetSpec): Record<string, unknown> {
const databaseURL = new URL(renderDatabaseURL(target, "<redacted>"));
return {
schema: target.database.schema,
dsnParameterPresence: {
sslmode: databaseURL.searchParams.has("sslmode"),
search_path: databaseURL.searchParams.has("search_path"),
},
valuesPrinted: false,
};
}
function readSecretMaterial(selection: Selection, target: TestTargetSpec): {
ok: boolean;
values: SecretMaterial;