mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
test(ql3): prove legacy env replay across promotion
This commit is contained in:
@@ -88,7 +88,7 @@
|
||||
|
||||
D-385~D-388 的 `config.sh`/Keyv/SSH data-directory lineage 与 SQLite `Envs` 保持分离;当前无稳定生产 schema 的历史 `Configs` 表继续 sealed+manual,
|
||||
不猜字段。ADR-0494 已关闭基础 Cluster mounted-files provider live 子门;后续切片仍必须完成固定低性能设备的真实 Edge 空间/写放大/断电证据,
|
||||
以及 Cluster Legacy Env migration 的专用 PostgreSQL SERIALIZABLE ledger、Task/Trigger revision mutation、直接外部 custody adapter 与 HA promotion 后 receipt replay。D-397 apply
|
||||
以及 Cluster Legacy Env migration 的专用 PostgreSQL SERIALIZABLE ledger、Task/Trigger revision mutation 与直接外部 custody adapter。HA promotion 后 receipt replay 已由 D-403/ADR-0498 关闭。D-397 apply
|
||||
只声明 sealed source retained 且 `physicalErasureGuaranteed=false`;明文销毁必须在 restart/
|
||||
readiness、观察窗和 rollback retention 之后另行强认证。Cluster 必须使用 PostgreSQL SERIALIZABLE ledger、外部 KMS/Secret provider 与 HA evidence,
|
||||
不复用 Local SQLite/POSIX authority,也不得把明文写入 PostgreSQL、ConfigMap、Pod env 或 Job command。
|
||||
@@ -174,8 +174,33 @@
|
||||
`a7c8a05e08c748d677475a09ce2998b741ed9326e27678613e93d431bc3769aa`;
|
||||
真实用例覆盖 Trigger 固定 Task r1、Task current r2、原子生成 Task r3/Trigger r2 并重定向 pin,
|
||||
同时证明 bundle ref-only execution、schedule reset、无流消费 replay 与数据库角色隔离。D-402
|
||||
关闭 mutation/receipt 边界;direct external custody、promotion 后 receipt replay 和固定低性能 Edge
|
||||
物理证据仍是 ADR-0491 转 Accepted 前的门禁。
|
||||
关闭 mutation/receipt 边界;相邻 D-403 已继续关闭 promotion 后 receipt replay,direct external
|
||||
custody 与固定低性能 Edge 物理证据仍是 ADR-0491 转 Accepted 前的门禁。
|
||||
|
||||
- D-403/ADR-0498(已验收):ADR-0497 application receipt 已进入真实 PostgreSQL 18 physical HA
|
||||
领域门,而不是继续引用通用表存活作为间接证据。主库与 standby 达到
|
||||
`synchronous_commit=remote_apply`/`sync_state=sync` 后,Automation Manager 提交真实 plan 与
|
||||
application;standby 在 recovery 中读取的 receipt、逐项 ledger、Task r3/execution、Trigger r2 与
|
||||
schedule fence 共 17 项 content-free facts 与主库完全一致。随后门禁制造 replication partition、
|
||||
fence 旧主库、把 standby 晋升到 timeline 2,并以 `pg_rewind --write-recovery-conf` 将旧主库重接为
|
||||
同步 standby。
|
||||
|
||||
写入能力恢复后,新的 Automation Manager Pool 重放同一 intent,必须返回 `existing`;Task/Trigger
|
||||
mutation stream factory 打开次数均为 0,application/Task/Trigger ledger 新增行均为 0,晋升后 17 项
|
||||
facts 仍与主库/standby 逐字段相同。独立 fixture 保持 disabled cron Trigger,避免污染 HA scheduler
|
||||
场景的候选集,但仍真实验证 schedule revision、state/claim fence 与 reset。HA private report 只包含
|
||||
count/revision/version/digest;evidence audit 强制 exact key set、三份 facts 同一性、两个 timeline state、
|
||||
replay side-effect 为零,并把 fixture SecretRef marker 加入 forbidden material。
|
||||
|
||||
PostgreSQL 18.6 arm64 HA 为 147/147 gates、timeline `1→2`,独立 audit 为
|
||||
`compatible=true/findings=[]`,最终报告 SHA-256 为
|
||||
`8bb61bc126ba96e7d4e20b1bfad4db960768c03473744d9d24e11b1b5b1a9286`。本切片不改 production
|
||||
source、schema/migration/ACL、package、依赖、镜像或 Edge import;完整 backend 为
|
||||
`1569 total / 1567 pass / 2 conditional skip / 0 fail`,四项 package/依赖/import 边界均 compatible。
|
||||
基础 Edge/Standalone 仍只包含 Local SQLite、runtime-core 与 SemVer,均为 325 files/58 modules,
|
||||
大小 `2,669,390 / 2,669,468 bytes`,距 4 MiB 上限保留 `1,524,914 / 1,524,836 bytes`;它只证明
|
||||
常驻闭包未扩大,不冒充真实设备证据。D-403 关闭 promotion replay;ADR-0491 现在只剩 direct
|
||||
external custody 与固定低性能 Edge 空间/写放大/断电恢复两项硬门。
|
||||
|
||||
- D-396/ADR-0490(已验收):Run History 不再只有永久 `manual_external`,但也没有被错误实现为 Legacy 日志到 3.0 Run ledger 的回灌。
|
||||
新的 Local adapter 以 ADR-0482 sealed capture bundle 作为 append-only 保全资产:Legacy history 必须逐事实选择 `retain_both`,Target history
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# ADR-0491:有界 Secret/Config Reconciliation 与任务环境绑定
|
||||
|
||||
- 状态:Proposed(D-397 已实现 Legacy Env inspection、私有有界 row plan、durable plan publication、独立 signed decision、逐项 Automation adoption provenance、Local SQLite 原子 application publisher、Owner prepared/apply/rollback 编排、ADR-0492 completion v3;ADR-0494 完成 Cluster mounted-files provider live 子门,ADR-0495 完成 content-free Cluster plan ledger,ADR-0496 完成 opaque environment bundle 数据面,ADR-0497 完成 Cluster Task/Trigger 原子 mutation 与 receipt;真实 Edge 空间证据、promotion 后 receipt replay 与直接外部 custody gate 尚未完成)
|
||||
- 状态:Proposed(D-397 已实现 Legacy Env inspection、私有有界 row plan、durable plan publication、独立 signed decision、逐项 Automation adoption provenance、Local SQLite 原子 application publisher、Owner prepared/apply/rollback 编排、ADR-0492 completion v3;ADR-0494 完成 Cluster mounted-files provider live 子门,ADR-0495 完成 content-free Cluster plan ledger,ADR-0496 完成 opaque environment bundle 数据面,ADR-0497 完成 Cluster Task/Trigger 原子 mutation 与 receipt,ADR-0498 完成 promotion 后 exact replay;真实 Edge 空间证据与直接外部 custody gate 尚未完成)
|
||||
- 日期:2026-08-23
|
||||
- 决策:D-397
|
||||
- 关联:ADR-0073、ADR-0074、ADR-0092、ADR-0094、ADR-0480、ADR-0482、ADR-0483、ADR-0484、ADR-0485、ADR-0486、ADR-0487、ADR-0488、ADR-0490
|
||||
- 关联:ADR-0073、ADR-0074、ADR-0092、ADR-0094、ADR-0480、ADR-0482、ADR-0483、ADR-0484、ADR-0485、ADR-0486、ADR-0487、ADR-0488、ADR-0490、ADR-0495、ADR-0496、ADR-0497、ADR-0498
|
||||
|
||||
## 背景
|
||||
|
||||
@@ -147,4 +147,4 @@ D-397 当前八切片已经实现:absent、unsupported、Edge over-budget、2.
|
||||
|
||||
ADR-0494 已完成 Cluster `mounted-files` provider live 子门:真实三节点 K3s 中两个 management replica、direct exact-key executor 和两个跨节点 provider observer 完成 PostgreSQL durable approval/binding、Kubernetes atomic projection rotation、无 Secret API 权限/ServiceAccount token、只读 `0440`、内容脱敏及删除后 fail-closed;v2 私有报告 24/24 gates 为 true,并保持 v1 verifier 兼容。该门不增加 Edge 闭包,也不等于直接 Vault/KMS/HSM custody。
|
||||
|
||||
转为 Accepted 前仍必须完成:固定低性能 Edge 设备的真实空间/写放大/断电恢复证据、直接外部 custody adapter,以及 HA promotion 后对既有 application receipt 的 exact replay。ADR-0495 已完成专用 PostgreSQL plan ledger,ADR-0496 已完成只保存 pinned bundle ref、通过 fenced remote delivery 取回 typed carrier 并在 Worker 内存展开的数据面;ADR-0497 又在一个 Project-serialized SERIALIZABLE transaction 中完成逐项 Task/Trigger current-head revalidation、revision/execution mutation、schedule reset 和 content-free append-only receipt,并支持合法历史 Task pin。它仍不写入 Secret material、不等于 direct Vault/KMS/HSM custody,也尚未在 promotion 后重放同一 application receipt。ADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,ADR-0493 又让没有 Legacy 身份输入的 fresh v52 目标身份经 signed `retain_target` 正确形成 no-effect,并精确消除六张已知目标表的 `unknown` 误判。Legacy `Auths/Users` 或真正未知表仍保持 manual;Local Owner 编排、mounted-files gate、plan/application ledger 或通用 PostgreSQL HA 证据都不得冒充完整外部密钥托管。
|
||||
转为 Accepted 前仍必须完成:固定低性能 Edge 设备的真实空间/写放大/断电恢复证据,以及直接外部 custody adapter。ADR-0495 已完成专用 PostgreSQL plan ledger,ADR-0496 已完成只保存 pinned bundle ref、通过 fenced remote delivery 取回 typed carrier 并在 Worker 内存展开的数据面;ADR-0497 又在一个 Project-serialized SERIALIZABLE transaction 中完成逐项 Task/Trigger current-head revalidation、revision/execution mutation、schedule reset 和 content-free append-only receipt,并支持合法历史 Task pin。ADR-0498 已证明该首次提交在 `remote_apply` 下到达 standby,并在 timeline `1→2`、`pg_rewind` 重建同步副本后由 Automation Manager 精确重放为 `existing`,不打开 mutation stream 或增加 durable row。它仍不写入 Secret material,也不等于 direct Vault/KMS/HSM custody。ADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,ADR-0493 又让没有 Legacy 身份输入的 fresh v52 目标身份经 signed `retain_target` 正确形成 no-effect,并精确消除六张已知目标表的 `unknown` 误判。Legacy `Auths/Users` 或真正未知表仍保持 manual;Local Owner 编排、mounted-files gate、plan/application ledger 或 HA replay 证据都不得冒充完整外部密钥托管。
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# ADR-0498:Cluster Legacy Env application 的 HA 晋升后精确重放
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-24
|
||||
- 决策:D-403
|
||||
- 关联:ADR-0092、ADR-0094、ADR-0491、ADR-0495、ADR-0496、ADR-0497
|
||||
|
||||
## 背景
|
||||
|
||||
ADR-0497 已让 Automation Manager 在一个 Project-serialized `SERIALIZABLE` transaction 中提交
|
||||
Legacy Env plan、Task/Trigger 新 revision、execution revision、schedule reset 与只追加 application
|
||||
receipt。此前 PostgreSQL HA 门只能证明通用 migration、role/readiness 及其他领域状态跨 timeline
|
||||
晋升存活,尚未证明这一新 application 的首次提交已同步复制,也未证明晋升后的同一 intent 会读取既有
|
||||
receipt,而不是重新打开大规模 mutation stream 或追加第二组 durable state。
|
||||
|
||||
通用 HA 成功不能替代领域重放。若 promotion 后只检查表存在,Task/Trigger head、execution digest、
|
||||
schedule fence 或逐项 receipt 仍可能漂移;若重放重新消费最多 100,000/500,000 项的输入流,则恢复成本
|
||||
不再有界,也可能把“检查既有提交”退化成第二次迁移。
|
||||
|
||||
## 决策
|
||||
|
||||
扩展既有 PostgreSQL 18 physical HA contract,不新增 workspace package、生产依赖、daemon、controller、
|
||||
timer、watcher 或部署资源。领域逻辑放在独立
|
||||
`scripts/ql3-postgres-ha-legacy-env-application-fixture.cjs`,14,000 行以上的 HA orchestration 只保留
|
||||
fixture import、主库调用、晋升后调用、report 与 gate 装配。
|
||||
|
||||
门禁固定执行以下顺序:
|
||||
|
||||
1. 主库与 standby 已进入 `synchronous_commit=remote_apply`、`sync_state=sync` 后,以 migration authority
|
||||
建立 active Project、Task r1、固定 Task r1 的 disabled cron Trigger r1,再把 Task 推进到 r2;
|
||||
2. 以 `ql3_automation_manager` 发布 content-free plan,并通过 ADR-0497 repository 原子生成 Task r3、
|
||||
Trigger r2、execution revision、schedule reset 与 application receipt;
|
||||
3. 在 standby 仍处于 recovery 时读取 17 项 content-free facts,要求与主库完全相同,然后才允许制造
|
||||
replication partition、fence 旧主库和 promotion;
|
||||
4. promotion 后先以 `pg_rewind --write-recovery-conf` 把旧主库作为同步 standby 重接,恢复
|
||||
`remote_apply` 写入能力,再使用新的 Automation Manager Pool 重放完全相同的 application intent;
|
||||
5. replay 必须返回 `existing`,Task/Trigger mutation stream factory 均不得被调用,receipt、逐项 ledger、
|
||||
Task/Trigger revision、execution digest 与 schedule fence 必须逐字段不变,新增 durable row 必须为 0。
|
||||
|
||||
Trigger fixture 保持 disabled,避免给 HA contract 中独立 scheduler failover 场景增加第二条合法可领取的
|
||||
schedule;它仍真实创建并迁移 cron schedule,因此会验证 revision、state/claim fence 与 reset。
|
||||
|
||||
## 证据边界
|
||||
|
||||
私有 HA report 只包含计数、revision、state/claim version 与 SHA-256 digest,不包含 Project/Task/Trigger
|
||||
ID、SecretRef、Env name/value、row body、ciphertext、key ID、数据库 DSN 或 credential。evidence audit
|
||||
现在强制要求三份事实对象只有固定 17 个 key、逐字段相同,并要求:
|
||||
|
||||
- `replicatedBeforePromotion=true`;
|
||||
- `exactReplayAfterPromotion=true` 且 `replayStatus=existing`;
|
||||
- `mutationStreamsOpenedAfterPromotion=0`;
|
||||
- `durableRowsAddedByReplay=0`;
|
||||
- 两个新增 timeline state 位于同步复制之后、partition/promotion 的正确顺序;
|
||||
- 缺证据、字段扩张、digest 非法、facts 漂移、SecretRef marker 泄漏或 gate 为 false 都失败关闭。
|
||||
|
||||
本 ADR 不声明 direct Vault/KMS/HSM custody,也不证明固定低性能 Edge 设备的空间、写放大或断电恢复;
|
||||
这两项仍是 ADR-0491 转 Accepted 前的独立门禁。mounted-files live rotation 也不能替代直接外部 custody。
|
||||
|
||||
## 验证
|
||||
|
||||
PostgreSQL 18.6 arm64 physical HA 通过 `147/147` gates,timeline `1→2`。主库提交、standby WAL
|
||||
投影与晋升后 replay 的 17 项 facts 完全一致;replay 返回 `existing`,mutation stream 打开次数与新增
|
||||
durable row 都为 0。独立 evidence audit 返回 `compatible=true`、`findings=[]`。最终私有报告
|
||||
SHA-256 为 `8bb61bc126ba96e7d4e20b1bfad4db960768c03473744d9d24e11b1b5b1a9286`。
|
||||
|
||||
本切片没有修改 production source、PostgreSQL schema/migration/ACL、package topology、依赖树、镜像或
|
||||
Edge/Standalone import graph;完整 backend 为 `1569 total / 1567 pass / 2 conditional skip / 0 fail`,
|
||||
package/Cluster dependency/122-module Edge/service bridge 边界均 compatible。基础 Edge/Standalone
|
||||
仍只包含 Local SQLite、runtime-core 与 SemVer,均为 325 files/58 modules,大小为
|
||||
`2,669,390 / 2,669,468 bytes`,距 4 MiB 上限保留 `1,524,914 / 1,524,836 bytes`。这些结果只证明
|
||||
常驻闭包未被 D-403 扩大,不冒充固定低性能设备的真实断电/写放大证据。D-403 把 ADR-0497 已有领域
|
||||
语义提升为 promotion 后可验证的发布证据。
|
||||
@@ -379,6 +379,11 @@ const {
|
||||
persistCancellationDispatchHaFixture,
|
||||
verifyPromotedCancellationDispatchHaFixture,
|
||||
} = require('./ql3-postgres-ha-cancellation-dispatch-fixture.cjs');
|
||||
const {
|
||||
clusterLegacyEnvMigrationApplicationFacts,
|
||||
persistClusterLegacyEnvMigrationApplicationBeforePromotion,
|
||||
verifyPromotedClusterLegacyEnvMigrationApplication,
|
||||
} = require('./ql3-postgres-ha-legacy-env-application-fixture.cjs');
|
||||
const {
|
||||
activateInstall,
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
@@ -11405,6 +11410,7 @@ async function main(argv = process.argv.slice(2)) {
|
||||
let runAttemptLogRetention;
|
||||
let manualRunRetry;
|
||||
let cancellationDispatch;
|
||||
let clusterLegacyEnvMigrationApplication;
|
||||
const startedAt = performance.now();
|
||||
const timeline = [];
|
||||
let report;
|
||||
@@ -11920,6 +11926,43 @@ async function main(argv = process.argv.slice(2)) {
|
||||
state: 'synchronous_remote_apply_ready',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
const legacyEnvAutomationDatabase = await databaseOpener(
|
||||
'automation-manager',
|
||||
databaseUrl(
|
||||
AUTOMATION_MANAGER_USER,
|
||||
AUTOMATION_MANAGER_PASSWORD,
|
||||
primaryPort,
|
||||
),
|
||||
'ql3-ha-legacy-env-application-primary',
|
||||
)();
|
||||
try {
|
||||
clusterLegacyEnvMigrationApplication =
|
||||
await persistClusterLegacyEnvMigrationApplicationBeforePromotion({
|
||||
migrationPool: primaryDatabase.pool,
|
||||
automationPool: legacyEnvAutomationDatabase.pool,
|
||||
});
|
||||
} finally {
|
||||
await legacyEnvAutomationDatabase.close();
|
||||
}
|
||||
const replicatedLegacyEnvApplication = await waitFor(async () => {
|
||||
const facts = await clusterLegacyEnvMigrationApplicationFacts(
|
||||
standbyDatabase.pool,
|
||||
clusterLegacyEnvMigrationApplication.fixture,
|
||||
);
|
||||
return JSON.stringify(facts) ===
|
||||
JSON.stringify(
|
||||
clusterLegacyEnvMigrationApplication.report.primaryBeforePromotion,
|
||||
)
|
||||
? facts
|
||||
: null;
|
||||
}, 'Cluster Legacy Env migration application WAL replay');
|
||||
clusterLegacyEnvMigrationApplication.report.standbyBeforePromotion =
|
||||
replicatedLegacyEnvApplication;
|
||||
clusterLegacyEnvMigrationApplication.report.replicatedBeforePromotion = true;
|
||||
timeline.push({
|
||||
state: 'cluster_legacy_env_application_replicated',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
await waitFor(async () => {
|
||||
const replicated = await copilotFailureDiagnosisAdmissionFacts(
|
||||
standbyDatabase.pool,
|
||||
@@ -13465,6 +13508,11 @@ async function main(argv = process.argv.slice(2)) {
|
||||
database: recoveredAutomationManagerDatabase,
|
||||
report: automationManagementInspection,
|
||||
});
|
||||
await verifyPromotedClusterLegacyEnvMigrationApplication({
|
||||
automationPool: recoveredAutomationManagerDatabase.pool,
|
||||
promotedPool: promotedDatabase.pool,
|
||||
evidence: clusterLegacyEnvMigrationApplication,
|
||||
});
|
||||
} finally {
|
||||
await recoveredAutomationManagerDatabase.close();
|
||||
}
|
||||
@@ -13472,6 +13520,10 @@ async function main(argv = process.argv.slice(2)) {
|
||||
state: 'automation_management_inspection_survived_promotion',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
timeline.push({
|
||||
state: 'cluster_legacy_env_application_replayed_after_promotion',
|
||||
atMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||
});
|
||||
await verifyPluginPackageQuarantineAfterPromotion({
|
||||
promotedPort: standbyPort,
|
||||
promotedDatabase,
|
||||
@@ -13954,6 +14006,8 @@ async function main(argv = process.argv.slice(2)) {
|
||||
approvalIdentityKeysetLedger,
|
||||
runManagementIdentityKeysetLedger,
|
||||
automationManagementInspection,
|
||||
clusterLegacyEnvMigrationApplication:
|
||||
clusterLegacyEnvMigrationApplication.report,
|
||||
pluginPackageLifecycle: pluginPackageLifecycle.report,
|
||||
pluginPackageQuarantine: pluginPackageQuarantine.report,
|
||||
pluginPackagePromptExecution: pluginPackageQuarantine.promptExecution,
|
||||
@@ -14312,6 +14366,30 @@ async function main(argv = process.argv.slice(2)) {
|
||||
.successfulReadAuditCount === 4 &&
|
||||
automationManagementInspection.afterPromotion.taskRevision === 1 &&
|
||||
automationManagementInspection.afterPromotion.triggerRevision === 1,
|
||||
clusterLegacyEnvMigrationApplicationExactlyReplaysAfterPromotion:
|
||||
clusterLegacyEnvMigrationApplication.report
|
||||
.replicatedBeforePromotion === true &&
|
||||
clusterLegacyEnvMigrationApplication.report
|
||||
.exactReplayAfterPromotion === true &&
|
||||
clusterLegacyEnvMigrationApplication.report.replayStatus ===
|
||||
'existing' &&
|
||||
clusterLegacyEnvMigrationApplication.report
|
||||
.mutationStreamsOpenedAfterPromotion === 0 &&
|
||||
clusterLegacyEnvMigrationApplication.report
|
||||
.durableRowsAddedByReplay === 0 &&
|
||||
JSON.stringify(
|
||||
clusterLegacyEnvMigrationApplication.report.primaryBeforePromotion,
|
||||
) ===
|
||||
JSON.stringify(
|
||||
clusterLegacyEnvMigrationApplication.report
|
||||
.standbyBeforePromotion,
|
||||
) &&
|
||||
JSON.stringify(
|
||||
clusterLegacyEnvMigrationApplication.report.primaryBeforePromotion,
|
||||
) ===
|
||||
JSON.stringify(
|
||||
clusterLegacyEnvMigrationApplication.report.promotedAfterReplay,
|
||||
),
|
||||
pluginPackageLifecycleCommitResponseLossConvergesExactlyOnce: true,
|
||||
pluginPackageLifecycleRunAndToolFencesTransitionAtomically: true,
|
||||
pluginPackageAutomationPublicationTransitionsAtomically:
|
||||
|
||||
@@ -15,11 +15,47 @@ const REQUIRED_TIMELINE_STATES = Object.freeze([
|
||||
'primary_ready',
|
||||
'standby_streaming',
|
||||
'synchronous_remote_apply_ready',
|
||||
'cluster_legacy_env_application_replicated',
|
||||
'replication_partition_and_promotion_guard_verified',
|
||||
'old_primary_fenced_and_admission_withdrawn',
|
||||
'cluster_legacy_env_application_replayed_after_promotion',
|
||||
'standby_promoted_old_primary_rejoined_endpoint_switched',
|
||||
'two_fresh_control_replicas_ready',
|
||||
]);
|
||||
const LEGACY_ENV_APPLICATION_FACT_KEYS = Object.freeze(
|
||||
[
|
||||
'executionContentDigest',
|
||||
'planRows',
|
||||
'receiptDigest',
|
||||
'receiptRows',
|
||||
'scheduleClaimVersion',
|
||||
'scheduleRevision',
|
||||
'scheduleStateVersion',
|
||||
'taskContentDigest',
|
||||
'taskCount',
|
||||
'taskItemRows',
|
||||
'taskRevision',
|
||||
'triggerContentDigest',
|
||||
'triggerCount',
|
||||
'triggerItemRows',
|
||||
'triggerRevision',
|
||||
'triggerTaskContentDigest',
|
||||
'triggerTaskRevision',
|
||||
].sort(),
|
||||
);
|
||||
const LEGACY_ENV_APPLICATION_KEYS = Object.freeze(
|
||||
[
|
||||
'contentFree',
|
||||
'durableRowsAddedByReplay',
|
||||
'exactReplayAfterPromotion',
|
||||
'mutationStreamsOpenedAfterPromotion',
|
||||
'primaryBeforePromotion',
|
||||
'promotedAfterReplay',
|
||||
'replayStatus',
|
||||
'replicatedBeforePromotion',
|
||||
'standbyBeforePromotion',
|
||||
].sort(),
|
||||
);
|
||||
const FORBIDDEN_MATERIAL = Object.freeze([
|
||||
'postgresql://',
|
||||
'ql3_migration_test',
|
||||
@@ -36,6 +72,8 @@ const FORBIDDEN_MATERIAL = Object.freeze([
|
||||
'ql3_worker_credential_executor_test',
|
||||
'ql3_worker_ingress_test',
|
||||
'ql3w_',
|
||||
'qlsecret:v1:',
|
||||
'ha-legacy-env-bundle-private',
|
||||
]);
|
||||
|
||||
function isObject(value) {
|
||||
@@ -250,6 +288,86 @@ function auditPostgresHaEvidence(report) {
|
||||
'UNCOMMITTED_WRITE_SURVIVED',
|
||||
);
|
||||
|
||||
const legacyEnvApplication = report.clusterLegacyEnvMigrationApplication;
|
||||
add(
|
||||
isObject(legacyEnvApplication),
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_EVIDENCE_MISSING',
|
||||
);
|
||||
if (isObject(legacyEnvApplication)) {
|
||||
add(
|
||||
JSON.stringify(Object.keys(legacyEnvApplication).sort()) ===
|
||||
JSON.stringify(LEGACY_ENV_APPLICATION_KEYS),
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_EVIDENCE_WIDENED',
|
||||
);
|
||||
add(
|
||||
legacyEnvApplication.replicatedBeforePromotion === true,
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_NOT_REPLICATED',
|
||||
);
|
||||
add(
|
||||
legacyEnvApplication.exactReplayAfterPromotion === true &&
|
||||
legacyEnvApplication.replayStatus === 'existing',
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_REPLAY_INVALID',
|
||||
);
|
||||
add(
|
||||
legacyEnvApplication.mutationStreamsOpenedAfterPromotion === 0 &&
|
||||
legacyEnvApplication.durableRowsAddedByReplay === 0,
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_REPLAY_SIDE_EFFECT',
|
||||
);
|
||||
add(
|
||||
legacyEnvApplication.contentFree === true,
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_EVIDENCE_NOT_CONTENT_FREE',
|
||||
);
|
||||
const factSets = [
|
||||
legacyEnvApplication.primaryBeforePromotion,
|
||||
legacyEnvApplication.standbyBeforePromotion,
|
||||
legacyEnvApplication.promotedAfterReplay,
|
||||
];
|
||||
for (const facts of factSets) {
|
||||
add(isObject(facts), 'CLUSTER_LEGACY_ENV_APPLICATION_FACTS_INVALID');
|
||||
if (!isObject(facts)) continue;
|
||||
add(
|
||||
JSON.stringify(Object.keys(facts).sort()) ===
|
||||
JSON.stringify(LEGACY_ENV_APPLICATION_FACT_KEYS),
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_FACTS_WIDENED',
|
||||
);
|
||||
add(
|
||||
facts.planRows === 1 &&
|
||||
facts.receiptRows === 1 &&
|
||||
facts.taskItemRows === 1 &&
|
||||
facts.triggerItemRows === 1 &&
|
||||
facts.taskCount === 1 &&
|
||||
facts.triggerCount === 1 &&
|
||||
facts.taskRevision === 3 &&
|
||||
facts.triggerRevision === 2 &&
|
||||
facts.triggerTaskRevision === 3 &&
|
||||
facts.scheduleRevision === 2 &&
|
||||
facts.scheduleStateVersion === 1 &&
|
||||
facts.scheduleClaimVersion === 1,
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_FACTS_DRIFTED',
|
||||
);
|
||||
for (const key of [
|
||||
'receiptDigest',
|
||||
'taskContentDigest',
|
||||
'executionContentDigest',
|
||||
'triggerContentDigest',
|
||||
'triggerTaskContentDigest',
|
||||
]) {
|
||||
add(
|
||||
typeof facts[key] === 'string' && SHA256_PATTERN.test(facts[key]),
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_DIGEST_INVALID',
|
||||
);
|
||||
}
|
||||
}
|
||||
add(
|
||||
factSets.every(
|
||||
(facts) =>
|
||||
isObject(facts) &&
|
||||
JSON.stringify(facts) === JSON.stringify(factSets[0]),
|
||||
),
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_FACTS_NOT_EXACT',
|
||||
);
|
||||
}
|
||||
|
||||
const gates = report.gates;
|
||||
add(isObject(gates), 'GATES_MISSING');
|
||||
if (isObject(gates)) {
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const {
|
||||
PostgresTaskDefinitionRepository,
|
||||
PostgresTriggerRepository,
|
||||
} = require('../packages/ql3-cluster-postgres/dist/entrypoints/admin.js');
|
||||
const {
|
||||
PostgresClusterLegacyEnvMigrationPlanRepository,
|
||||
} = require('../packages/ql3-cluster-postgres/dist/reconciliation/clusterLegacyEnvMigrationPlanRepository.js');
|
||||
const {
|
||||
PostgresClusterLegacyEnvMigrationApplicationRepository,
|
||||
} = require('../packages/ql3-cluster-postgres/dist/reconciliation/clusterLegacyEnvMigrationApplicationRepository.js');
|
||||
const {
|
||||
createClusterLegacyEnvMigrationTaskMutationSetDigester,
|
||||
createClusterLegacyEnvMigrationTriggerMutationSetDigester,
|
||||
} = require('../packages/ql3-runtime-core/dist/migration/clusterLegacyEnvMigrationApplication.js');
|
||||
const {
|
||||
createSecretRef,
|
||||
} = require('../packages/ql3-runtime-core/dist/secret/secretReference.js');
|
||||
|
||||
const FIXTURE = Object.freeze({
|
||||
projectId: 'ha-legacy-env-application',
|
||||
taskId: 'ha legacy env task',
|
||||
triggerId: 'ha legacy env trigger',
|
||||
planId: 'ha-legacy-env-plan',
|
||||
planMutationId: '819f7900-0000-4000-8000-000000000001',
|
||||
applicationId: 'ha-legacy-env-application-receipt',
|
||||
applicationMutationId: '819f7900-0000-4000-8000-000000000002',
|
||||
taskCreateMutationId: '819f7900-0000-4000-8000-000000000003',
|
||||
triggerCreateMutationId: '819f7900-0000-4000-8000-000000000004',
|
||||
taskAdvanceMutationId: '819f7900-0000-4000-8000-000000000005',
|
||||
taskApplicationMutationId: '819f7900-0000-4000-8000-000000000006',
|
||||
triggerApplicationMutationId: '819f7900-0000-4000-8000-000000000007',
|
||||
privateBundleName: 'ha-legacy-env-bundle-private',
|
||||
});
|
||||
|
||||
async function clusterLegacyEnvMigrationApplicationFacts(pool, fixture) {
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
receipt.receipt_digest AS "receiptDigest",
|
||||
receipt.task_count AS "taskCount",
|
||||
receipt.trigger_count AS "triggerCount",
|
||||
task_item.revision AS "taskRevision",
|
||||
task_item.content_digest AS "taskContentDigest",
|
||||
task_item.execution_content_digest AS "executionContentDigest",
|
||||
trigger_item.revision AS "triggerRevision",
|
||||
trigger_item.content_digest AS "triggerContentDigest",
|
||||
trigger_item.task_revision AS "triggerTaskRevision",
|
||||
trigger_item.task_content_digest AS "triggerTaskContentDigest",
|
||||
schedule.trigger_revision AS "scheduleRevision",
|
||||
schedule.state_version AS "scheduleStateVersion",
|
||||
schedule.claim_version AS "scheduleClaimVersion",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_plans" AS plan
|
||||
WHERE plan.plan_id = receipt.plan_id) AS "planRows",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_receipts" AS stored
|
||||
WHERE stored.application_id = receipt.application_id) AS "receiptRows",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_tasks" AS item
|
||||
WHERE item.application_id = receipt.application_id) AS "taskItemRows",
|
||||
(SELECT count(*)::integer
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_triggers" AS item
|
||||
WHERE item.application_id = receipt.application_id) AS "triggerItemRows"
|
||||
FROM "ql3"."cluster_legacy_env_migration_application_receipts" AS receipt
|
||||
JOIN "ql3"."cluster_legacy_env_migration_application_tasks" AS task_item
|
||||
ON task_item.application_id = receipt.application_id
|
||||
AND task_item.ordinal = 0
|
||||
JOIN "ql3"."cluster_legacy_env_migration_application_triggers" AS trigger_item
|
||||
ON trigger_item.application_id = receipt.application_id
|
||||
AND trigger_item.ordinal = 0
|
||||
JOIN "ql3"."trigger_schedules" AS schedule
|
||||
ON schedule.project_id = trigger_item.project_id
|
||||
AND schedule.trigger_id = trigger_item.trigger_id
|
||||
WHERE receipt.application_id = $1`,
|
||||
[fixture.applicationId],
|
||||
);
|
||||
assert.equal(result.rowCount, 1);
|
||||
return Object.freeze({ ...result.rows[0] });
|
||||
}
|
||||
|
||||
function assertExpectedFacts(facts) {
|
||||
assert.equal(facts.planRows, 1);
|
||||
assert.equal(facts.receiptRows, 1);
|
||||
assert.equal(facts.taskItemRows, 1);
|
||||
assert.equal(facts.triggerItemRows, 1);
|
||||
assert.equal(facts.taskCount, 1);
|
||||
assert.equal(facts.triggerCount, 1);
|
||||
assert.equal(facts.taskRevision, 3);
|
||||
assert.equal(facts.triggerRevision, 2);
|
||||
assert.equal(facts.triggerTaskRevision, 3);
|
||||
assert.equal(facts.scheduleRevision, 2);
|
||||
assert.equal(facts.scheduleStateVersion, 1);
|
||||
assert.equal(facts.scheduleClaimVersion, 1);
|
||||
for (const digest of [
|
||||
facts.receiptDigest,
|
||||
facts.taskContentDigest,
|
||||
facts.executionContentDigest,
|
||||
facts.triggerContentDigest,
|
||||
facts.triggerTaskContentDigest,
|
||||
]) {
|
||||
assert.match(digest, /^[0-9a-f]{64}$/);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistClusterLegacyEnvMigrationApplicationBeforePromotion(
|
||||
options,
|
||||
) {
|
||||
const { migrationPool, automationPool } = options;
|
||||
const observed = await migrationPool.query(
|
||||
`SELECT floor(extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
||||
AS "observedAtMs"`,
|
||||
);
|
||||
const occurredAtMs = Number(observed.rows[0].observedAtMs) - 1_000;
|
||||
await migrationPool.query(
|
||||
`INSERT INTO "ql3"."projects" (
|
||||
id, name, slug, status, version, created_at_ms, updated_at_ms
|
||||
) VALUES ($1, 'HA Legacy Env application', $1, 'active', 1, $2, $2)`,
|
||||
[FIXTURE.projectId, occurredAtMs],
|
||||
);
|
||||
|
||||
const tasks = new PostgresTaskDefinitionRepository(migrationPool);
|
||||
const triggers = new PostgresTriggerRepository(migrationPool);
|
||||
const initialTask = (
|
||||
await tasks.appendTaskDefinitionRevision({
|
||||
projectId: FIXTURE.projectId,
|
||||
taskId: FIXTURE.taskId,
|
||||
expectedRevision: null,
|
||||
mutationId: FIXTURE.taskCreateMutationId,
|
||||
name: 'HA Legacy Env command',
|
||||
description: 'promotion replay fixture',
|
||||
kind: 'command',
|
||||
spec: {
|
||||
schema: 'qinglong/command@v1',
|
||||
config: {
|
||||
command: { kind: 'argv', file: '/bin/echo', args: ['ha'] },
|
||||
timeoutMs: 30_000,
|
||||
},
|
||||
},
|
||||
labels: { 'qinglong.io/source': 'ha-contract' },
|
||||
enabled: true,
|
||||
occurredAtMs,
|
||||
})
|
||||
).definition;
|
||||
const initialTrigger = (
|
||||
await triggers.appendTriggerRevision({
|
||||
projectId: FIXTURE.projectId,
|
||||
triggerId: FIXTURE.triggerId,
|
||||
expectedRevision: null,
|
||||
mutationId: FIXTURE.triggerCreateMutationId,
|
||||
taskId: FIXTURE.taskId,
|
||||
taskRevision: initialTask.revision,
|
||||
taskContentDigest: initialTask.contentDigest,
|
||||
spec: {
|
||||
schema: 'qinglong/cron@v1',
|
||||
config: {
|
||||
expression: '* * * * *',
|
||||
timezone: 'UTC',
|
||||
misfirePolicy: 'skip',
|
||||
},
|
||||
},
|
||||
enabled: false,
|
||||
occurredAtMs: occurredAtMs + 1,
|
||||
})
|
||||
).trigger;
|
||||
const currentTask = (
|
||||
await tasks.appendTaskDefinitionRevision({
|
||||
projectId: FIXTURE.projectId,
|
||||
taskId: FIXTURE.taskId,
|
||||
expectedRevision: initialTask.revision,
|
||||
mutationId: FIXTURE.taskAdvanceMutationId,
|
||||
name: initialTask.name,
|
||||
description: initialTask.description,
|
||||
kind: initialTask.kind,
|
||||
spec: initialTask.spec,
|
||||
labels: initialTask.labels,
|
||||
enabled: initialTask.enabled,
|
||||
occurredAtMs: occurredAtMs + 2,
|
||||
})
|
||||
).definition;
|
||||
|
||||
const taskMutations = Object.freeze([
|
||||
Object.freeze({
|
||||
ordinal: 0,
|
||||
taskId: FIXTURE.taskId,
|
||||
previousRevision: currentTask.revision,
|
||||
previousContentDigest: currentTask.contentDigest,
|
||||
mutationId: FIXTURE.taskApplicationMutationId,
|
||||
}),
|
||||
]);
|
||||
const triggerMutations = Object.freeze([
|
||||
Object.freeze({
|
||||
ordinal: 0,
|
||||
triggerId: FIXTURE.triggerId,
|
||||
taskId: FIXTURE.taskId,
|
||||
previousRevision: initialTrigger.revision,
|
||||
previousContentDigest: initialTrigger.contentDigest,
|
||||
previousTaskRevision: initialTask.revision,
|
||||
previousTaskContentDigest: initialTask.contentDigest,
|
||||
mutationId: FIXTURE.triggerApplicationMutationId,
|
||||
}),
|
||||
]);
|
||||
const taskDigester = createClusterLegacyEnvMigrationTaskMutationSetDigester();
|
||||
taskDigester.update(taskMutations[0]);
|
||||
const taskSet = taskDigester.finish();
|
||||
const triggerDigester =
|
||||
createClusterLegacyEnvMigrationTriggerMutationSetDigester();
|
||||
triggerDigester.update(triggerMutations[0]);
|
||||
const triggerSet = triggerDigester.finish();
|
||||
const secretRef = createSecretRef({
|
||||
projectId: FIXTURE.projectId,
|
||||
name: FIXTURE.privateBundleName,
|
||||
version: 1,
|
||||
});
|
||||
const plan = (
|
||||
await new PostgresClusterLegacyEnvMigrationPlanRepository(
|
||||
automationPool,
|
||||
).publish({
|
||||
planId: FIXTURE.planId,
|
||||
mutationId: FIXTURE.planMutationId,
|
||||
projectId: FIXTURE.projectId,
|
||||
source: {
|
||||
reconciliationBundleDigest: '1'.repeat(64),
|
||||
decisionDigest: '2'.repeat(64),
|
||||
candidateSetDigest: '3'.repeat(64),
|
||||
sourceRowCount: 1,
|
||||
activeRowCount: 1,
|
||||
disabledRowCount: 0,
|
||||
effectiveBindingCount: 1,
|
||||
},
|
||||
target: {
|
||||
secretRef,
|
||||
taskRevisionSetDigest: taskSet.revisionSetDigest,
|
||||
triggerRevisionSetDigest: triggerSet.revisionSetDigest,
|
||||
taskCount: taskSet.count,
|
||||
triggerCount: triggerSet.count,
|
||||
totalEffectiveBytes: 128,
|
||||
},
|
||||
})
|
||||
).plan;
|
||||
const intent = Object.freeze({
|
||||
applicationId: FIXTURE.applicationId,
|
||||
mutationId: FIXTURE.applicationMutationId,
|
||||
projectId: FIXTURE.projectId,
|
||||
planId: FIXTURE.planId,
|
||||
planDigest: plan.planDigest,
|
||||
taskMutationSetDigest: taskSet.mutationSetDigest,
|
||||
triggerMutationSetDigest: triggerSet.mutationSetDigest,
|
||||
});
|
||||
let taskStreamOpenCount = 0;
|
||||
let triggerStreamOpenCount = 0;
|
||||
const applications =
|
||||
new PostgresClusterLegacyEnvMigrationApplicationRepository(automationPool);
|
||||
const applied = await applications.apply(intent, {
|
||||
taskMutations() {
|
||||
taskStreamOpenCount += 1;
|
||||
return taskMutations;
|
||||
},
|
||||
triggerMutations() {
|
||||
triggerStreamOpenCount += 1;
|
||||
return triggerMutations;
|
||||
},
|
||||
});
|
||||
assert.equal(applied.status, 'applied');
|
||||
assert.equal(taskStreamOpenCount, 1);
|
||||
assert.equal(triggerStreamOpenCount, 1);
|
||||
const primaryBeforePromotion =
|
||||
await clusterLegacyEnvMigrationApplicationFacts(migrationPool, FIXTURE);
|
||||
assertExpectedFacts(primaryBeforePromotion);
|
||||
return {
|
||||
fixture: Object.freeze({
|
||||
...FIXTURE,
|
||||
intent,
|
||||
receipt: applied.receipt,
|
||||
}),
|
||||
report: {
|
||||
primaryBeforePromotion,
|
||||
standbyBeforePromotion: null,
|
||||
promotedAfterReplay: null,
|
||||
replicatedBeforePromotion: false,
|
||||
exactReplayAfterPromotion: false,
|
||||
replayStatus: null,
|
||||
mutationStreamsOpenedAfterPromotion: null,
|
||||
durableRowsAddedByReplay: null,
|
||||
contentFree: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function verifyPromotedClusterLegacyEnvMigrationApplication(options) {
|
||||
const { automationPool, promotedPool, evidence } = options;
|
||||
let mutationStreamsOpened = 0;
|
||||
const applications =
|
||||
new PostgresClusterLegacyEnvMigrationApplicationRepository(automationPool);
|
||||
const beforeReplay = await clusterLegacyEnvMigrationApplicationFacts(
|
||||
promotedPool,
|
||||
evidence.fixture,
|
||||
);
|
||||
assert.deepEqual(beforeReplay, evidence.report.primaryBeforePromotion);
|
||||
const replay = await applications.apply(evidence.fixture.intent, {
|
||||
taskMutations() {
|
||||
mutationStreamsOpened += 1;
|
||||
throw new Error('exact replay must not reopen Task mutations');
|
||||
},
|
||||
triggerMutations() {
|
||||
mutationStreamsOpened += 1;
|
||||
throw new Error('exact replay must not reopen Trigger mutations');
|
||||
},
|
||||
});
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.deepEqual(replay.receipt, evidence.fixture.receipt);
|
||||
const promotedAfterReplay = await clusterLegacyEnvMigrationApplicationFacts(
|
||||
promotedPool,
|
||||
evidence.fixture,
|
||||
);
|
||||
assertExpectedFacts(promotedAfterReplay);
|
||||
assert.deepEqual(promotedAfterReplay, beforeReplay);
|
||||
evidence.report.promotedAfterReplay = promotedAfterReplay;
|
||||
evidence.report.exactReplayAfterPromotion = true;
|
||||
evidence.report.replayStatus = replay.status;
|
||||
evidence.report.mutationStreamsOpenedAfterPromotion = mutationStreamsOpened;
|
||||
evidence.report.durableRowsAddedByReplay =
|
||||
promotedAfterReplay.receiptRows - beforeReplay.receiptRows;
|
||||
return evidence.report;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clusterLegacyEnvMigrationApplicationFacts,
|
||||
persistClusterLegacyEnvMigrationApplicationBeforePromotion,
|
||||
verifyPromotedClusterLegacyEnvMigrationApplication,
|
||||
};
|
||||
@@ -6,6 +6,28 @@ const {
|
||||
auditPostgresHaEvidence,
|
||||
} = require('../../scripts/ql3-postgres-ha-evidence-audit.cjs');
|
||||
|
||||
function legacyEnvApplicationFacts() {
|
||||
return {
|
||||
receiptDigest: '1'.repeat(64),
|
||||
taskCount: 1,
|
||||
triggerCount: 1,
|
||||
taskRevision: 3,
|
||||
taskContentDigest: '2'.repeat(64),
|
||||
executionContentDigest: '3'.repeat(64),
|
||||
triggerRevision: 2,
|
||||
triggerContentDigest: '4'.repeat(64),
|
||||
triggerTaskRevision: 3,
|
||||
triggerTaskContentDigest: '5'.repeat(64),
|
||||
scheduleRevision: 2,
|
||||
scheduleStateVersion: 1,
|
||||
scheduleClaimVersion: 1,
|
||||
planRows: 1,
|
||||
receiptRows: 1,
|
||||
taskItemRows: 1,
|
||||
triggerItemRows: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(architecture = 'arm64') {
|
||||
const gates = { passed: true };
|
||||
for (let index = 0; index < 100; index += 1) gates[`gate${index}`] = true;
|
||||
@@ -66,12 +88,25 @@ function fixture(architecture = 'arm64') {
|
||||
synchronousState: 'sync',
|
||||
rejoinedAsWritablePrimary: false,
|
||||
},
|
||||
clusterLegacyEnvMigrationApplication: {
|
||||
primaryBeforePromotion: legacyEnvApplicationFacts(),
|
||||
standbyBeforePromotion: legacyEnvApplicationFacts(),
|
||||
promotedAfterReplay: legacyEnvApplicationFacts(),
|
||||
replicatedBeforePromotion: true,
|
||||
exactReplayAfterPromotion: true,
|
||||
replayStatus: 'existing',
|
||||
mutationStreamsOpenedAfterPromotion: 0,
|
||||
durableRowsAddedByReplay: 0,
|
||||
contentFree: true,
|
||||
},
|
||||
timeline: [
|
||||
'primary_ready',
|
||||
'standby_streaming',
|
||||
'synchronous_remote_apply_ready',
|
||||
'cluster_legacy_env_application_replicated',
|
||||
'replication_partition_and_promotion_guard_verified',
|
||||
'old_primary_fenced_and_admission_withdrawn',
|
||||
'cluster_legacy_env_application_replayed_after_promotion',
|
||||
'standby_promoted_old_primary_rejoined_endpoint_switched',
|
||||
'two_fresh_control_replicas_ready',
|
||||
].map((state, atMs) => ({ state, atMs })),
|
||||
@@ -92,11 +127,18 @@ test('rejects false gates, promotion drift and hidden private material', () => {
|
||||
const report = fixture();
|
||||
report.gates.oldPrimaryFencedBeforePromotion = false;
|
||||
report.replication.promotedPrimaryTimeline = 1;
|
||||
report.clusterLegacyEnvMigrationApplication.promotedAfterReplay.receiptRows = 2;
|
||||
report.privateValue = 'postgresql://private-credential';
|
||||
const result = auditPostgresHaEvidence(report);
|
||||
assert.equal(result.compatible, false);
|
||||
assert.ok(result.findings.includes('GATE_FAILED'));
|
||||
assert.ok(result.findings.includes('PROMOTION_TIMELINE_INVALID'));
|
||||
assert.ok(
|
||||
result.findings.includes('CLUSTER_LEGACY_ENV_APPLICATION_FACTS_DRIFTED'),
|
||||
);
|
||||
assert.ok(
|
||||
result.findings.includes('CLUSTER_LEGACY_ENV_APPLICATION_FACTS_NOT_EXACT'),
|
||||
);
|
||||
assert.ok(result.findings.includes('PRIVATE_MATERIAL_PRESENT'));
|
||||
});
|
||||
|
||||
@@ -109,3 +151,27 @@ test('rejects reordered timeline and limitations drift', () => {
|
||||
assert.ok(result.findings.includes('LIMITATIONS_DRIFTED'));
|
||||
assert.ok(result.findings.some((finding) => finding.startsWith('TIMELINE_')));
|
||||
});
|
||||
|
||||
test('rejects missing, widened or private Legacy Env replay evidence', () => {
|
||||
const missing = fixture();
|
||||
delete missing.clusterLegacyEnvMigrationApplication;
|
||||
assert.ok(
|
||||
auditPostgresHaEvidence(missing).findings.includes(
|
||||
'CLUSTER_LEGACY_ENV_APPLICATION_EVIDENCE_MISSING',
|
||||
),
|
||||
);
|
||||
|
||||
const widened = fixture();
|
||||
widened.clusterLegacyEnvMigrationApplication.privateRef =
|
||||
'qlsecret:v1:private';
|
||||
widened.clusterLegacyEnvMigrationApplication.primaryBeforePromotion.secretRef =
|
||||
'ha-legacy-env-bundle-private';
|
||||
const result = auditPostgresHaEvidence(widened);
|
||||
assert.ok(
|
||||
result.findings.includes('CLUSTER_LEGACY_ENV_APPLICATION_EVIDENCE_WIDENED'),
|
||||
);
|
||||
assert.ok(
|
||||
result.findings.includes('CLUSTER_LEGACY_ENV_APPLICATION_FACTS_WIDENED'),
|
||||
);
|
||||
assert.ok(result.findings.includes('PRIVATE_MATERIAL_PRESENT'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user