mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): gate console capacity evidence
This commit is contained in:
@@ -542,7 +542,7 @@ jobs:
|
|||||||
- name: Verify native runner architecture
|
- name: Verify native runner architecture
|
||||||
run: node -e "if (process.arch !== '${{ matrix.node_arch }}') throw new Error('unexpected architecture ' + process.arch)"
|
run: node -e "if (process.arch !== '${{ matrix.node_arch }}') throw new Error('unexpected architecture ' + process.arch)"
|
||||||
- name: Test exact SBOM and release contract failures
|
- name: Test exact SBOM and release contract failures
|
||||||
run: node --test test/back/ql3ClusterImageSbom.test.cjs test/back/ql3ClusterImageReleaseAudit.test.cjs
|
run: node --test test/back/ql3ClusterImageSbom.test.cjs test/back/ql3ClusterImageReleaseAudit.test.cjs test/back/ql3ClusterCopilotConsoleCapacityEvidence.test.cjs
|
||||||
- name: Audit deployment and image release contracts
|
- name: Audit deployment and image release contracts
|
||||||
run: |
|
run: |
|
||||||
pnpm audit:cluster-deployment:ql3
|
pnpm audit:cluster-deployment:ql3
|
||||||
@@ -594,6 +594,40 @@ jobs:
|
|||||||
IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }}
|
IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }}
|
||||||
QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'
|
QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'
|
||||||
run: node scripts/ql3-cluster-admin-product-live-contract.cjs --image="${IMAGE}"
|
run: node scripts/ql3-cluster-admin-product-live-contract.cjs --image="${IMAGE}"
|
||||||
|
- name: Capture the fixed Cluster Copilot Console capacity envelope
|
||||||
|
if: matrix.image == 'admin'
|
||||||
|
timeout-minutes: 10
|
||||||
|
env:
|
||||||
|
IMAGE: qinglong3-cluster-admin:ci-${{ matrix.image_arch }}
|
||||||
|
QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '1'
|
||||||
|
SOURCE_REPOSITORY: ${{ github.repository }}
|
||||||
|
SOURCE_REVISION: ${{ github.sha }}
|
||||||
|
SOURCE_WORKFLOW: ${{ github.workflow }}
|
||||||
|
SOURCE_RUN_ID: ${{ github.run_id }}
|
||||||
|
SOURCE_RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||||
|
run: |
|
||||||
|
mkdir -p "${RUNNER_TEMP}/ql3-cluster-console-capacity"
|
||||||
|
node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs \
|
||||||
|
--mode=capture \
|
||||||
|
--repository="${SOURCE_REPOSITORY}" \
|
||||||
|
--revision="${SOURCE_REVISION}" \
|
||||||
|
--workflow="${SOURCE_WORKFLOW}" \
|
||||||
|
--run-id="${SOURCE_RUN_ID}" \
|
||||||
|
--run-attempt="${SOURCE_RUN_ATTEMPT}" \
|
||||||
|
--architecture="${{ matrix.node_arch }}" \
|
||||||
|
--image="${IMAGE}" \
|
||||||
|
--output="${RUNNER_TEMP}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json"
|
||||||
|
- name: Upload native Cluster Copilot Console capacity evidence
|
||||||
|
if: matrix.image == 'admin'
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.node_arch }}
|
||||||
|
path: ${{ runner.temp }}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
compression-level: 0
|
||||||
|
overwrite: false
|
||||||
|
include-hidden-files: false
|
||||||
- name: Generate the reviewed application SBOM
|
- name: Generate the reviewed application SBOM
|
||||||
run: >-
|
run: >-
|
||||||
node scripts/ql3-cluster-image-sbom.cjs
|
node scripts/ql3-cluster-image-sbom.cjs
|
||||||
@@ -614,6 +648,65 @@ jobs:
|
|||||||
--image=${{ matrix.image }}
|
--image=${{ matrix.image }}
|
||||||
--inventory-root=/opt/qinglong/node_modules
|
--inventory-root=/opt/qinglong/node_modules
|
||||||
|
|
||||||
|
cluster-console-capacity-release-evidence:
|
||||||
|
name: Cross-architecture Cluster Copilot Console capacity evidence
|
||||||
|
needs: cluster-image
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 5
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24.18.0'
|
||||||
|
- name: Download native x64 Console capacity evidence
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-x64
|
||||||
|
path: ${{ runner.temp }}/ql3-cluster-console-capacity/x64
|
||||||
|
- name: Download native arm64 Console capacity evidence
|
||||||
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-arm64
|
||||||
|
path: ${{ runner.temp }}/ql3-cluster-console-capacity/arm64
|
||||||
|
- name: Merge and audit the source-bound Console capacity evidence
|
||||||
|
env:
|
||||||
|
SOURCE_REPOSITORY: ${{ github.repository }}
|
||||||
|
SOURCE_REVISION: ${{ github.sha }}
|
||||||
|
SOURCE_WORKFLOW: ${{ github.workflow }}
|
||||||
|
SOURCE_RUN_ID: ${{ github.run_id }}
|
||||||
|
SOURCE_RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||||
|
run: |
|
||||||
|
node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs \
|
||||||
|
--mode=merge \
|
||||||
|
--repository="${SOURCE_REPOSITORY}" \
|
||||||
|
--revision="${SOURCE_REVISION}" \
|
||||||
|
--workflow="${SOURCE_WORKFLOW}" \
|
||||||
|
--run-id="${SOURCE_RUN_ID}" \
|
||||||
|
--run-attempt="${SOURCE_RUN_ATTEMPT}" \
|
||||||
|
--x64="${RUNNER_TEMP}/ql3-cluster-console-capacity/x64/x64.json" \
|
||||||
|
--arm64="${RUNNER_TEMP}/ql3-cluster-console-capacity/arm64/arm64.json" \
|
||||||
|
--output="${RUNNER_TEMP}/ql3-cluster-console-capacity/cross-architecture.json"
|
||||||
|
node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs \
|
||||||
|
--mode=audit \
|
||||||
|
--repository="${SOURCE_REPOSITORY}" \
|
||||||
|
--revision="${SOURCE_REVISION}" \
|
||||||
|
--workflow="${SOURCE_WORKFLOW}" \
|
||||||
|
--run-id="${SOURCE_RUN_ID}" \
|
||||||
|
--run-attempt="${SOURCE_RUN_ATTEMPT}" \
|
||||||
|
--report="${RUNNER_TEMP}/ql3-cluster-console-capacity/cross-architecture.json"
|
||||||
|
- name: Upload cross-architecture Console capacity evidence
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: ql3-cluster-console-capacity-release-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
path: ${{ runner.temp }}/ql3-cluster-console-capacity/cross-architecture.json
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
compression-level: 0
|
||||||
|
overwrite: false
|
||||||
|
include-hidden-files: false
|
||||||
|
|
||||||
image-oci:
|
image-oci:
|
||||||
name: ${{ matrix.image }} multi-architecture OCI evidence
|
name: ${{ matrix.image }} multi-architecture OCI evidence
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
|
|||||||
@@ -11,6 +11,23 @@
|
|||||||
|
|
||||||
最新增量证据(2026-08-20):
|
最新增量证据(2026-08-20):
|
||||||
|
|
||||||
|
- D-370/ADR-0463(实现与本地协议门已接受;首份原生 CI 双架构报告待实际 workflow 产生):为显式按需的 Cluster Admin
|
||||||
|
Copilot Console 增加固定 Linux x64/arm64 容量与 assertion 生命周期发布证据。两个原生 Admin image matrix job 分别在
|
||||||
|
`192 MiB / swap 0 / 0.25 CPU / 32 PIDs`、只读 root、非 root、cap-drop ALL、no-new-privileges、默认 seccomp、8 MiB tmpfs 与
|
||||||
|
loopback-only publication 下执行四次用户驱动读取,并从 Console 自身 cgroup v2 采集 memory max/peak/events、swap、CPU 与 PID;要求至少
|
||||||
|
32 MiB headroom 且 `max/oom/oom_kill/oom_group_kill` 不增加。隔离 synthetic TLS 1.3/mTLS management verifier 验证
|
||||||
|
`initial accepted → atomic rotated accepted → expired rejected → rotated recovered`,Console 全程不重启、不轮询、不重试、不缓存、不 mutation,过期
|
||||||
|
assertion 只投影为 502/`assertion_expired` 低敏事实。每架构报告绑定 repository/revision/workflow/run ID/attempt 和独立 image ID,使用 nofollow
|
||||||
|
bounded input、`wx/0600` output 与 domain-separated canonical SHA-256;独立只读 job 以 commit-pinned artifact actions 精确下载、合并并离线重审。
|
||||||
|
实现保持在单一 CI 脚本和既有 image release audit 中,没有新增 package、dependency、binary、服务、端口、数据库对象或部署 workload,避免制造单文件微包;
|
||||||
|
Edge/Standalone closure 不含 Console。D-370 聚焦证据与发布审计为 `108/108`,连同 image SBOM 的原生 job 聚焦集合为 `120/120`;18-package
|
||||||
|
clean build/逐包测试退出 0,完整 backend 工作区门为 `1,503 total / 1,501 pass / 2 conditional skip / 0 fail`(含一条不会提交的既有用户测试;D-370
|
||||||
|
提交范围为 `1,502 total / 1,500 pass / 2 skip`),六项架构审计和 14 档 Local artifact
|
||||||
|
audit 全部 compatible。基础 Edge/Standalone 仍为 `2,589,998 / 2,590,076` bytes,Application+AI 为 `4,493,151 / 4,493,283` bytes,MCP 为
|
||||||
|
`7,315,930 / 7,316,038` bytes。本机只能验证协议、Docker inspect 契约和 workflow 装配,尚无本提交的原生
|
||||||
|
GitHub x64/arm64 memory peak、artifact digest 或 run URL,因此不能把实现状态写成最终现场证据。192 MiB 只回答 workstation Console 的有界空载读取,
|
||||||
|
不是物理路由器最低配置,也不是 Cluster 节点吞吐、故障恢复或容量规划。
|
||||||
|
|
||||||
- D-369/ADR-0462(已接受):在既有 operator-workstation Copilot Console 内完成显式可选、默认关闭的 Run management 只读纵切,新增固定
|
- D-369/ADR-0462(已接受):在既有 operator-workstation Copilot Console 内完成显式可选、默认关闭的 Run management 只读纵切,新增固定
|
||||||
`run_cancellation_status`、`run_cancellation_blocked_list`、`run_cancellation_inspect` 三种 browser/BFF operation。只有同时提供独立
|
`run_cancellation_status`、`run_cancellation_blocked_list`、`run_cancellation_inspect` 三种 browser/BFF operation。只有同时提供独立
|
||||||
`--run-management-config` 与 owner-private `--run-management-assertion` 才启用,Project API credential、浏览器 session 与 Run 专用 TLS 1.3/mTLS/OIDC
|
`--run-management-config` 与 owner-private `--run-management-assertion` 才启用,Project API credential、浏览器 session 与 Run 专用 TLS 1.3/mTLS/OIDC
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# ADR-0463:原生双架构 Console 容量与 Assertion 生命周期证据
|
||||||
|
|
||||||
|
- 状态:Accepted(证据协议、原生采集与 CI 聚合门已实现;首份 GitHub Actions 双架构报告待实际运行产生)
|
||||||
|
- 日期:2026-08-20
|
||||||
|
- 关联 RFC:QL-RFC-0001 D-370、PR-5、PR-7
|
||||||
|
- 关联 ADR:ADR-0088、ADR-0281、ADR-0322、ADR-0329、ADR-0462
|
||||||
|
- Amends:ADR-0462 中尚未取得的 compact Console Linux x64/arm64 实测边界
|
||||||
|
|
||||||
|
## 上下文
|
||||||
|
|
||||||
|
D-369 已把 Run cancellation status、blocked page 与 inspect 作为显式可选、用户驱动的只读纵切接入
|
||||||
|
Cluster Copilot Console,并继续声明 compact workstation 容器预算为 `192 MiB / 0.25 CPU / 32 PIDs`。
|
||||||
|
静态 limit、macOS Docker Desktop 或单架构开发机都不能证明生产镜像在原生 Linux x64 与 arm64 上确实满足该
|
||||||
|
envelope,也不能证明 Console 在不重启时会为每次点击重新读取轮换后的短期 assertion,并低敏拒绝过期凭据。
|
||||||
|
|
||||||
|
部署用户跨度很大:Edge/Standalone 可能运行在小型路由设备,而 Cluster operator workstation 与集群节点有
|
||||||
|
完全不同的资源和可用性要求。因此本门只能回答“按需启动的 Cluster Admin Console 是否在固定 CI 容器预算内
|
||||||
|
完成四次有界管理读取”,不能把结果外推为物理 Edge 最低配置、Cluster 节点吞吐或多节点容量规划。
|
||||||
|
|
||||||
|
## 决策
|
||||||
|
|
||||||
|
1. 新增单一 CI/审核脚本 `scripts/ql3-cluster-copilot-console-capacity-evidence.cjs`,提供 `capture`、`merge`、
|
||||||
|
`audit` 三种模式。它不新增 workspace package、生产 dependency、binary、服务、数据库对象或部署 workload;
|
||||||
|
证据职责不足以成立独立 package,避免再次制造只有一个文件的微包。
|
||||||
|
2. `capture` 只允许 Node `v24.18.0` 的原生 Linux `x64` 或 `arm64`,并要求显式
|
||||||
|
`QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE=1`。镜像必须是当前 matrix 刚构建的独立 Admin image,
|
||||||
|
architecture、content ID、size 和 `10001:10001` runtime user 均从 Docker inspect 取得。
|
||||||
|
3. Console 使用精确 compact envelope:`192 MiB` memory、memory-swap 与 memory 相同从而使 cgroup v2
|
||||||
|
`memory.swap.max=0`、`0.25 CPU`、`32 PIDs`、只读 root、cap-drop ALL、no-new-privileges、默认 seccomp、
|
||||||
|
`8 MiB` noexec/nosuid/nodev tmpfs、非 root user 和仅 `127.0.0.1` 发布端口。authority volume 只读挂载,
|
||||||
|
management fixture 只存在于隔离 Docker network 且不发布宿主端口。
|
||||||
|
4. 在四次请求前后从 Console 自身 cgroup v2 读取 `memory.max`、`memory.peak`、`memory.swap.max`、
|
||||||
|
`memory.events`、`cpu.max`、`pids.max/current`,并从 `/proc/self/status` 读取 NoNewPrivs/seccomp。peak 必须至少
|
||||||
|
保留 `32 MiB` headroom;`max/oom/oom_kill/oom_group_kill` 不能增加。container start identity 前后相同,
|
||||||
|
因而不能通过重启掩盖累积峰值或 assertion reload 缺陷。
|
||||||
|
5. synthetic management verifier 只接受 TLS 1.3 和受信 client certificate,固定 service SAN 与
|
||||||
|
`/api/v3/runs/management`,且只记录 label、TLS、method、path、operation、mutation 等低敏事实。它不是外部
|
||||||
|
IdP 或生产 authorization attestation。
|
||||||
|
6. 用户驱动序列固定为 `initial_accepted → rotated_accepted → expired_rejected → rotated_recovered`。Assertion
|
||||||
|
通过同一只读 volume 内的 `write wx → chown → atomic rename` 原位轮换;Console 每次 POST 重新读取文件,
|
||||||
|
不能重启、轮询、重试、缓存或触发 mutation。过期 assertion 必须由上游 401 被 BFF 投影为 502 和
|
||||||
|
`assertion_expired`,响应与日志不能泄露 assertion、authority path 或 service identity。
|
||||||
|
7. 每个原生 matrix job 生成 exact-shape、source-bound 架构报告,绑定 repository、40 位 revision、workflow、
|
||||||
|
run ID 与 run attempt。输入为有大小上限、nofollow 的普通 JSON;输出只以 `wx/0600` 新建。内容使用固定
|
||||||
|
domain separator 的 canonical SHA-256,未知字段、非有限数字、过深或过多节点均失败关闭。
|
||||||
|
8. Artifact 名精确包含 run ID、attempt 与 architecture,upload/download action 固定到完整 commit,禁止
|
||||||
|
overwrite。独立只读 job 必须等待完整 `cluster-image` matrix,分别下载 x64/arm64,不使用 pattern 或
|
||||||
|
merge-multiple;merge 重验同源与不同 image ID,随后在同一 job 离线 audit 合并报告。
|
||||||
|
9. 合并报告显式保留四条 limitation:192 MiB workstation envelope 不是 Cluster 吞吐/容量规划;native CI
|
||||||
|
不是物理 Edge 最低配置、断电、闪存、热环境或 soak 证明;synthetic mTLS verifier 不是外部 IdP 证明;
|
||||||
|
workflow source binding 不是密码学硬件 attestation。
|
||||||
|
|
||||||
|
## 被拒绝的替代方案
|
||||||
|
|
||||||
|
### 在 macOS arm64 上用 QEMU 生成双架构结果
|
||||||
|
|
||||||
|
拒绝。标签与模拟执行不能代替两个原生 Linux runner;本地只能验证协议、负向门禁和工作流装配。
|
||||||
|
|
||||||
|
### 把容量采集拆成多个 workspace package
|
||||||
|
|
||||||
|
拒绝。capture、merge、audit 只服务一个 CI evidence contract,没有独立生产依赖、版本、入口或运行时生命周期。
|
||||||
|
拆分会扩大 package 数和维护面,并重现 `packages/*/src` 只有一个平铺文件的问题。
|
||||||
|
|
||||||
|
### 把 192 MiB 写成路由设备最低配置
|
||||||
|
|
||||||
|
拒绝。Console 是显式按需的 Cluster operator workstation 工具,Edge/Standalone artifact closure 不包含它;
|
||||||
|
固定路由器仍需明确硬件、内核、文件系统、断电、闪存、热环境和长时间 soak 证据。
|
||||||
|
|
||||||
|
### 把空载 Console 峰值写成 Cluster 容量
|
||||||
|
|
||||||
|
拒绝。该门只有四次串行读,没有副本、数据库延迟、并发 operator、Worker、queue、failover 或恢复负载。
|
||||||
|
Cluster 容量规划必须由独立多节点负载与故障恢复门完成。
|
||||||
|
|
||||||
|
### 为 assertion 轮换加入 watcher、polling 或重启
|
||||||
|
|
||||||
|
拒绝。既有 server 的每次点击读取已经提供最小、可审计的 reload 语义;watcher/timer 会增加 idle 成本,重启会
|
||||||
|
破坏 session 并掩盖实际 reload 行为。
|
||||||
|
|
||||||
|
## 验证与当前证据状态
|
||||||
|
|
||||||
|
- D-370 证据协议与 CI 发布审计聚焦门为 `108/108`;原生 `cluster-image` 聚焦集合连同 SBOM 为 `120/120`。覆盖双架构正向 merge/audit、headroom、swap、OOM、PID、
|
||||||
|
schema widening、assertion sequence/TLS/mTLS/mutation、digest 篡改、跨 run、重复 image、symlink、overwrite、
|
||||||
|
live opt-in,以及 capture、matrix dependency、offline audit 的负向 workflow drift。
|
||||||
|
- 本机 Docker inspect 探针确认 SecurityOpt 为 `no-new-privileges`,tmpfs 保留精确 `size=8m` 表示;探针容器已
|
||||||
|
删除。该探针只用于修正静态 inspect 契约,不冒充 native Linux x64/arm64 容量证据。
|
||||||
|
- CI 脚本与 workflow 已实现,但当前提交尚未在 GitHub Actions 产生同一 run 的两个原生 architecture artifact。
|
||||||
|
因此本 ADR 接受的是实现和门禁设计,不能宣称已经取得最终 memory peak、image ID、artifact digest 或 run URL。
|
||||||
|
- 该变化不改 package/dependency tree、Edge/Standalone runtime closure、PostgreSQL schema/role/Pool 或 HA 拓扑,
|
||||||
|
不重新占有数据库 HA 证据。
|
||||||
|
- 18-package clean build 与逐包测试在允许 loopback listener 的宿主环境退出 0;Worker Runtime 独立复核为
|
||||||
|
`133/133`。当前完整 backend 工作区门为 `1,503 total / 1,501 pass / 2 conditional skip / 0 fail`;其中包含一项
|
||||||
|
与本切片无关、保持未跟踪且不会提交的用户测试,因此 D-370 提交范围对应 `1,502 total / 1,500 pass / 2 skip`。package boundary、dependency、Edge import、Cluster deployment、
|
||||||
|
Console 与 Console distribution 六项审计均 compatible、零 finding,workspace 仍为 18 packages、无 single/shallow
|
||||||
|
package,Cluster Admin 为 `125 source / 124 nested`。
|
||||||
|
- 14 档 Local artifact audit 全部 compatible;基础 Edge/Standalone 精确为 `2,589,998 / 2,590,076` bytes,
|
||||||
|
Application+AI 为 `4,493,151 / 4,493,283` bytes,MCP 为 `7,315,930 / 7,316,038` bytes,证明 CI-only evidence
|
||||||
|
没有进入低配部署闭包。
|
||||||
|
|
||||||
|
## 后续边界
|
||||||
|
|
||||||
|
- 推送后记录首个成功 workflow run、x64/arm64 bundle digest、cross-architecture release digest、各架构 image ID/
|
||||||
|
size/memory peak/headroom 与 artifact retention;若任何架构超过 envelope,必须修实现或调整经过 RFC 评审的
|
||||||
|
budget,不能放宽 validator 伪造成功。
|
||||||
|
- 物理 Edge 支持下限继续由 ADR-0088 类型的固定设备矩阵证明;Cluster 节点容量继续由多节点数据库、Worker、
|
||||||
|
queue、failover 与恢复负载证明。
|
||||||
|
- 后续若 Console 引入新的 read operation 或生产 dependency,必须重新采集双架构报告并证明仍无 mutation、
|
||||||
|
polling、authority leak 和 Edge closure 变化。
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,8 @@ function auditClusterImageCiWorkflow(
|
|||||||
const workflow = yaml.load(source);
|
const workflow = yaml.load(source);
|
||||||
const clusterImageJob = workflow?.jobs?.['cluster-image'];
|
const clusterImageJob = workflow?.jobs?.['cluster-image'];
|
||||||
const localImageJob = workflow?.jobs?.['local-image'];
|
const localImageJob = workflow?.jobs?.['local-image'];
|
||||||
|
const consoleCapacityJob =
|
||||||
|
workflow?.jobs?.['cluster-console-capacity-release-evidence'];
|
||||||
const expectedNativeMatrix = [
|
const expectedNativeMatrix = [
|
||||||
{
|
{
|
||||||
runner: 'ubuntu-24.04',
|
runner: 'ubuntu-24.04',
|
||||||
@@ -282,6 +284,11 @@ function auditClusterImageCiWorkflow(
|
|||||||
/pnpm audit:image-release:ql3/,
|
/pnpm audit:image-release:ql3/,
|
||||||
'image CI must audit the shared release workflow contract',
|
'image CI must audit the shared release workflow contract',
|
||||||
);
|
);
|
||||||
|
requirePattern(
|
||||||
|
source,
|
||||||
|
/test\/back\/ql3ClusterCopilotConsoleCapacityEvidence\.test\.cjs/,
|
||||||
|
'native image CI must run the Console capacity evidence protocol tests',
|
||||||
|
);
|
||||||
requirePattern(
|
requirePattern(
|
||||||
source,
|
source,
|
||||||
/pnpm audit:deployment-lock-surfaces:ql3/,
|
/pnpm audit:deployment-lock-surfaces:ql3/,
|
||||||
@@ -341,6 +348,135 @@ function auditClusterImageCiWorkflow(
|
|||||||
/name: Run the bounded Cluster Admin product facade\s+if: matrix\.image == 'admin'\s+env:\s+IMAGE: qinglong3-cluster-admin:ci-\$\{\{ matrix\.image_arch \}\}\s+QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'\s+run: node scripts\/ql3-cluster-admin-product-live-contract\.cjs --image="\$\{IMAGE\}"/,
|
/name: Run the bounded Cluster Admin product facade\s+if: matrix\.image == 'admin'\s+env:\s+IMAGE: qinglong3-cluster-admin:ci-\$\{\{ matrix\.image_arch \}\}\s+QL3_CLUSTER_ADMIN_PRODUCT_LIVE: '1'\s+run: node scripts\/ql3-cluster-admin-product-live-contract\.cjs --image="\$\{IMAGE\}"/,
|
||||||
'native admin image CI must run the bounded product facade contract',
|
'native admin image CI must run the bounded product facade contract',
|
||||||
);
|
);
|
||||||
|
const capacityCapture = clusterImageJob?.steps?.find(
|
||||||
|
({ name }) =>
|
||||||
|
name === 'Capture the fixed Cluster Copilot Console capacity envelope',
|
||||||
|
);
|
||||||
|
const nativeCapacityUpload = clusterImageJob?.steps?.find(
|
||||||
|
({ name }) =>
|
||||||
|
name === 'Upload native Cluster Copilot Console capacity evidence',
|
||||||
|
);
|
||||||
|
const expectedSourceEnvironment = {
|
||||||
|
SOURCE_REPOSITORY: '${{ github.repository }}',
|
||||||
|
SOURCE_REVISION: '${{ github.sha }}',
|
||||||
|
SOURCE_WORKFLOW: '${{ github.workflow }}',
|
||||||
|
SOURCE_RUN_ID: '${{ github.run_id }}',
|
||||||
|
SOURCE_RUN_ATTEMPT: '${{ github.run_attempt }}',
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
capacityCapture?.if !== "matrix.image == 'admin'" ||
|
||||||
|
capacityCapture?.['timeout-minutes'] !== 10 ||
|
||||||
|
JSON.stringify(capacityCapture?.env) !==
|
||||||
|
JSON.stringify({
|
||||||
|
IMAGE: 'qinglong3-cluster-admin:ci-${{ matrix.image_arch }}',
|
||||||
|
QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '1',
|
||||||
|
...expectedSourceEnvironment,
|
||||||
|
}) ||
|
||||||
|
typeof capacityCapture?.run !== 'string' ||
|
||||||
|
!capacityCapture.run.includes(
|
||||||
|
'node scripts/ql3-cluster-copilot-console-capacity-evidence.cjs',
|
||||||
|
) ||
|
||||||
|
!capacityCapture.run.includes('--mode=capture') ||
|
||||||
|
!capacityCapture.run.includes('--architecture="${{ matrix.node_arch }}"') ||
|
||||||
|
!capacityCapture.run.includes('--image="${IMAGE}"') ||
|
||||||
|
!capacityCapture.run.includes(
|
||||||
|
'--output="${RUNNER_TEMP}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json"',
|
||||||
|
) ||
|
||||||
|
nativeCapacityUpload?.if !== "matrix.image == 'admin'" ||
|
||||||
|
nativeCapacityUpload?.uses !==
|
||||||
|
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' ||
|
||||||
|
JSON.stringify(nativeCapacityUpload?.with) !==
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.node_arch }}',
|
||||||
|
path: '${{ runner.temp }}/ql3-cluster-console-capacity/${{ matrix.node_arch }}.json',
|
||||||
|
'if-no-files-found': 'error',
|
||||||
|
'retention-days': 14,
|
||||||
|
'compression-level': 0,
|
||||||
|
overwrite: false,
|
||||||
|
'include-hidden-files': false,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'native admin image CI must capture and retain the exact source-bound Console capacity envelope',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const capacitySteps = consoleCapacityJob?.steps;
|
||||||
|
const x64Download = capacitySteps?.find(
|
||||||
|
({ name }) => name === 'Download native x64 Console capacity evidence',
|
||||||
|
);
|
||||||
|
const arm64Download = capacitySteps?.find(
|
||||||
|
({ name }) => name === 'Download native arm64 Console capacity evidence',
|
||||||
|
);
|
||||||
|
const capacityMerge = capacitySteps?.find(
|
||||||
|
({ name }) =>
|
||||||
|
name === 'Merge and audit the source-bound Console capacity evidence',
|
||||||
|
);
|
||||||
|
const capacityUpload = capacitySteps?.find(
|
||||||
|
({ name }) =>
|
||||||
|
name === 'Upload cross-architecture Console capacity evidence',
|
||||||
|
);
|
||||||
|
const downloadAction =
|
||||||
|
'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c';
|
||||||
|
if (
|
||||||
|
consoleCapacityJob?.needs !== 'cluster-image' ||
|
||||||
|
consoleCapacityJob?.['runs-on'] !== 'ubuntu-24.04' ||
|
||||||
|
consoleCapacityJob?.['timeout-minutes'] !== 5 ||
|
||||||
|
JSON.stringify(consoleCapacityJob?.permissions) !==
|
||||||
|
JSON.stringify({ contents: 'read' }) ||
|
||||||
|
!Array.isArray(capacitySteps) ||
|
||||||
|
capacitySteps.length !== 6 ||
|
||||||
|
capacitySteps[0]?.uses !== 'actions/checkout@v6' ||
|
||||||
|
capacitySteps[1]?.uses !== 'actions/setup-node@v6' ||
|
||||||
|
JSON.stringify(capacitySteps[1]?.with) !==
|
||||||
|
JSON.stringify({ 'node-version': '24.18.0' }) ||
|
||||||
|
x64Download?.uses !== downloadAction ||
|
||||||
|
JSON.stringify(x64Download?.with) !==
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-x64',
|
||||||
|
path: '${{ runner.temp }}/ql3-cluster-console-capacity/x64',
|
||||||
|
}) ||
|
||||||
|
arm64Download?.uses !== downloadAction ||
|
||||||
|
JSON.stringify(arm64Download?.with) !==
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'ql3-cluster-console-capacity-${{ github.run_id }}-${{ github.run_attempt }}-arm64',
|
||||||
|
path: '${{ runner.temp }}/ql3-cluster-console-capacity/arm64',
|
||||||
|
}) ||
|
||||||
|
JSON.stringify(capacityMerge?.env) !==
|
||||||
|
JSON.stringify(expectedSourceEnvironment) ||
|
||||||
|
typeof capacityMerge?.run !== 'string' ||
|
||||||
|
(
|
||||||
|
capacityMerge.run.match(
|
||||||
|
/node scripts\/ql3-cluster-copilot-console-capacity-evidence\.cjs/g,
|
||||||
|
) || []
|
||||||
|
).length !== 2 ||
|
||||||
|
!capacityMerge.run.includes('--mode=merge') ||
|
||||||
|
!capacityMerge.run.includes('--mode=audit') ||
|
||||||
|
!capacityMerge.run.includes(
|
||||||
|
'--x64="${RUNNER_TEMP}/ql3-cluster-console-capacity/x64/x64.json"',
|
||||||
|
) ||
|
||||||
|
!capacityMerge.run.includes(
|
||||||
|
'--arm64="${RUNNER_TEMP}/ql3-cluster-console-capacity/arm64/arm64.json"',
|
||||||
|
) ||
|
||||||
|
!capacityMerge.run.includes(
|
||||||
|
'--report="${RUNNER_TEMP}/ql3-cluster-console-capacity/cross-architecture.json"',
|
||||||
|
) ||
|
||||||
|
capacityUpload?.uses !==
|
||||||
|
'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' ||
|
||||||
|
JSON.stringify(capacityUpload?.with) !==
|
||||||
|
JSON.stringify({
|
||||||
|
name: 'ql3-cluster-console-capacity-release-${{ github.run_id }}-${{ github.run_attempt }}',
|
||||||
|
path: '${{ runner.temp }}/ql3-cluster-console-capacity/cross-architecture.json',
|
||||||
|
'if-no-files-found': 'error',
|
||||||
|
'retention-days': 14,
|
||||||
|
'compression-level': 0,
|
||||||
|
overwrite: false,
|
||||||
|
'include-hidden-files': false,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Console capacity release evidence must merge and audit exact native x64 and arm64 reports with read-only authority',
|
||||||
|
);
|
||||||
|
}
|
||||||
requirePattern(
|
requirePattern(
|
||||||
adminProductLiveContract,
|
adminProductLiveContract,
|
||||||
/runOperatorContextContract\(image\);[\s\S]*operatorContext: true,[\s\S]*contextPreflight: true,[\s\S]*contextReadiness: true/,
|
/runOperatorContextContract\(image\);[\s\S]*operatorContext: true,[\s\S]*contextPreflight: true,[\s\S]*contextReadiness: true/,
|
||||||
@@ -390,6 +526,14 @@ function auditClusterImageCiWorkflow(
|
|||||||
clusterAdminOperatorContext: true,
|
clusterAdminOperatorContext: true,
|
||||||
clusterAdminContextPreflight: true,
|
clusterAdminContextPreflight: true,
|
||||||
clusterAdminContextReadiness: true,
|
clusterAdminContextReadiness: true,
|
||||||
|
clusterCopilotConsoleCapacityEvidence: {
|
||||||
|
nativeArchitectures: ['x64', 'arm64'],
|
||||||
|
memoryLimitMiB: 192,
|
||||||
|
minimumHeadroomMiB: 32,
|
||||||
|
assertionRotation: true,
|
||||||
|
assertionExpiryRejected: true,
|
||||||
|
sourceBound: true,
|
||||||
|
},
|
||||||
releaseVersionAudit: true,
|
releaseVersionAudit: true,
|
||||||
deploymentLockMaterialization: true,
|
deploymentLockMaterialization: true,
|
||||||
ociAttestations: true,
|
ociAttestations: true,
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { spawnSync } = require('node:child_process');
|
||||||
|
const { test } = require('node:test');
|
||||||
|
|
||||||
|
const {
|
||||||
|
ASSERTION_SEQUENCE,
|
||||||
|
LIMITATIONS,
|
||||||
|
MEMORY_MAX_BYTES,
|
||||||
|
MINIMUM_MEMORY_HEADROOM_BYTES,
|
||||||
|
createArchitectureEvidence,
|
||||||
|
evidenceDigest,
|
||||||
|
mergeCrossArchitectureEvidence,
|
||||||
|
normalizeSource,
|
||||||
|
readJsonFile,
|
||||||
|
validateArchitectureEvidence,
|
||||||
|
validateObservation,
|
||||||
|
validateReleaseEvidence,
|
||||||
|
} = require('../../scripts/ql3-cluster-copilot-console-capacity-evidence.cjs');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '../..');
|
||||||
|
const scriptPath = path.join(
|
||||||
|
root,
|
||||||
|
'scripts/ql3-cluster-copilot-console-capacity-evidence.cjs',
|
||||||
|
);
|
||||||
|
const scriptSource = fs.readFileSync(scriptPath, 'utf8');
|
||||||
|
|
||||||
|
function fixtureSource(overrides = {}) {
|
||||||
|
return {
|
||||||
|
repository: 'whyour/qinglong',
|
||||||
|
revision: 'a'.repeat(40),
|
||||||
|
workflow: 'QingLong 3.0 CI',
|
||||||
|
runId: '123456',
|
||||||
|
runAttempt: 1,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function memoryEvents(overrides = {}) {
|
||||||
|
return {
|
||||||
|
low: 0,
|
||||||
|
high: 0,
|
||||||
|
max: 0,
|
||||||
|
oom: 0,
|
||||||
|
oomKill: 0,
|
||||||
|
oomGroupKill: 0,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixtureObservation(architecture, overrides = {}) {
|
||||||
|
const peak = 96 * 1024 * 1024;
|
||||||
|
return {
|
||||||
|
schemaVersion: 1,
|
||||||
|
observedAtMs: 1_700_000_000_000,
|
||||||
|
platform: 'linux',
|
||||||
|
architecture,
|
||||||
|
image: {
|
||||||
|
architecture: architecture === 'x64' ? 'amd64' : 'arm64',
|
||||||
|
id: `sha256:${architecture === 'x64' ? '1' : '2'}`.padEnd(
|
||||||
|
71,
|
||||||
|
architecture === 'x64' ? '1' : '2',
|
||||||
|
),
|
||||||
|
bytes: architecture === 'x64' ? 120_000_000 : 119_000_000,
|
||||||
|
user: '10001:10001',
|
||||||
|
},
|
||||||
|
runtime: { node: 'v24.18.0', uid: 10001, gid: 10001 },
|
||||||
|
envelope: {
|
||||||
|
memoryMaxBytes: MEMORY_MAX_BYTES,
|
||||||
|
memoryPeakBytes: peak,
|
||||||
|
memoryHeadroomBytes: MEMORY_MAX_BYTES - peak,
|
||||||
|
swapMaxBytes: 0,
|
||||||
|
cpuQuotaMicros: 25_000,
|
||||||
|
cpuPeriodMicros: 100_000,
|
||||||
|
pidsMax: 32,
|
||||||
|
pidsCurrent: 5,
|
||||||
|
noNewPrivileges: 1,
|
||||||
|
seccompMode: 2,
|
||||||
|
readOnlyRoot: true,
|
||||||
|
tmpfsBytes: 8 * 1024 * 1024,
|
||||||
|
publishedHostAddress: '127.0.0.1',
|
||||||
|
capabilityDrop: 'ALL',
|
||||||
|
memoryEventsBefore: memoryEvents(),
|
||||||
|
memoryEventsAfter: memoryEvents(),
|
||||||
|
},
|
||||||
|
assertionLifecycle: {
|
||||||
|
requestCount: 4,
|
||||||
|
sequence: [...ASSERTION_SEQUENCE],
|
||||||
|
tlsVersion: 'TLSv1.3',
|
||||||
|
mutualTls: true,
|
||||||
|
consoleRestarted: false,
|
||||||
|
mutation: false,
|
||||||
|
operation: 'run.cancellation.summary',
|
||||||
|
expiredConsoleStatus: 502,
|
||||||
|
expiredCode: 'assertion_expired',
|
||||||
|
},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function fixtureArchitecture(architecture, source = fixtureSource()) {
|
||||||
|
return createArchitectureEvidence({
|
||||||
|
source,
|
||||||
|
architecture,
|
||||||
|
observation: fixtureObservation(architecture),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceArguments(source = fixtureSource()) {
|
||||||
|
return [
|
||||||
|
`--repository=${source.repository}`,
|
||||||
|
`--revision=${source.revision}`,
|
||||||
|
`--workflow=${source.workflow}`,
|
||||||
|
`--run-id=${source.runId}`,
|
||||||
|
`--run-attempt=${source.runAttempt}`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJson(filePath, value) {
|
||||||
|
fs.writeFileSync(filePath, `${JSON.stringify(value)}\n`, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCli(arguments_, env = {}) {
|
||||||
|
return spawnSync(process.execPath, [scriptPath, ...arguments_], {
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('creates exact native reports and a source-bound cross-architecture release report', () => {
|
||||||
|
const source = fixtureSource();
|
||||||
|
const x64 = fixtureArchitecture('x64', source);
|
||||||
|
const arm64 = fixtureArchitecture('arm64', source);
|
||||||
|
const release = mergeCrossArchitectureEvidence({ source, x64, arm64 });
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
x64.fixture,
|
||||||
|
'qinglong/cluster-console-capacity-architecture-evidence@v1',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
release.fixture,
|
||||||
|
'qinglong/cluster-console-capacity-cross-architecture-evidence@v1',
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
release.architectures.map(({ architecture }) => architecture),
|
||||||
|
['x64', 'arm64'],
|
||||||
|
);
|
||||||
|
assert.equal(release.gates.releaseEvidenceComplete, true);
|
||||||
|
assert.equal(release.gates.passed, true);
|
||||||
|
assert.deepEqual(release.assertionLifecycle.sequence, ASSERTION_SEQUENCE);
|
||||||
|
assert.deepEqual(release.limitations, LIMITATIONS);
|
||||||
|
assert.equal(release.releaseDigest.length, 64);
|
||||||
|
assert.notEqual(x64.bundleDigest, arm64.bundleDigest);
|
||||||
|
assert.equal(validateReleaseEvidence(release, source), release);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects memory pressure, OOM, swap, PID and widened envelope observations', () => {
|
||||||
|
const architecture = 'x64';
|
||||||
|
const base = fixtureObservation(architecture);
|
||||||
|
const cases = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
envelope: {
|
||||||
|
...base.envelope,
|
||||||
|
memoryPeakBytes: MEMORY_MAX_BYTES - MINIMUM_MEMORY_HEADROOM_BYTES + 1,
|
||||||
|
memoryHeadroomBytes: MINIMUM_MEMORY_HEADROOM_BYTES - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/resource envelope drifted/,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
envelope: {
|
||||||
|
...base.envelope,
|
||||||
|
memoryEventsAfter: memoryEvents({ oomKill: 1 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/memory event oomKill changed/,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ envelope: { ...base.envelope, swapMaxBytes: 1024 } },
|
||||||
|
/resource envelope drifted/,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ envelope: { ...base.envelope, pidsCurrent: 33 } },
|
||||||
|
/resource envelope drifted/,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ envelope: { ...base.envelope, unexpected: true } },
|
||||||
|
/envelope fields are invalid/,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
for (const [override, expected] of cases) {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateObservation(
|
||||||
|
fixtureObservation(architecture, override),
|
||||||
|
architecture,
|
||||||
|
),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects assertion lifecycle, native identity and mutation drift', () => {
|
||||||
|
const base = fixtureObservation('arm64');
|
||||||
|
for (const assertionLifecycle of [
|
||||||
|
{ ...base.assertionLifecycle, requestCount: 3 },
|
||||||
|
{
|
||||||
|
...base.assertionLifecycle,
|
||||||
|
sequence: ['initial_accepted', 'expired_rejected'],
|
||||||
|
},
|
||||||
|
{ ...base.assertionLifecycle, tlsVersion: 'TLSv1.2' },
|
||||||
|
{ ...base.assertionLifecycle, mutualTls: false },
|
||||||
|
{ ...base.assertionLifecycle, consoleRestarted: true },
|
||||||
|
{ ...base.assertionLifecycle, mutation: true },
|
||||||
|
{ ...base.assertionLifecycle, operation: 'run.cancellation.rearm' },
|
||||||
|
{ ...base.assertionLifecycle, expiredCode: 'assertion_invalid' },
|
||||||
|
]) {
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateObservation(
|
||||||
|
fixtureObservation('arm64', { assertionLifecycle }),
|
||||||
|
'arm64',
|
||||||
|
),
|
||||||
|
/assertion lifecycle drifted/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.throws(
|
||||||
|
() => validateObservation(fixtureObservation('arm64'), 'x64'),
|
||||||
|
/native identity is invalid/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects tampering, cross-run mixing and duplicate image identity', () => {
|
||||||
|
const source = fixtureSource();
|
||||||
|
const x64 = fixtureArchitecture('x64', source);
|
||||||
|
const arm64 = fixtureArchitecture('arm64', source);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
validateArchitectureEvidence(
|
||||||
|
{ ...x64, bundleDigest: '0'.repeat(64) },
|
||||||
|
source,
|
||||||
|
'x64',
|
||||||
|
),
|
||||||
|
/digest or gates drifted/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
mergeCrossArchitectureEvidence({
|
||||||
|
source,
|
||||||
|
x64,
|
||||||
|
arm64: fixtureArchitecture(
|
||||||
|
'arm64',
|
||||||
|
fixtureSource({ revision: 'b'.repeat(40) }),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/belongs to another source/,
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateObservation = fixtureObservation('arm64');
|
||||||
|
duplicateObservation.image.id = x64.observation.image.id;
|
||||||
|
const duplicate = createArchitectureEvidence({
|
||||||
|
source,
|
||||||
|
architecture: 'arm64',
|
||||||
|
observation: duplicateObservation,
|
||||||
|
});
|
||||||
|
assert.throws(
|
||||||
|
() => mergeCrossArchitectureEvidence({ source, x64, arm64: duplicate }),
|
||||||
|
/independently measured images/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects coercible source fields and oversized canonical evidence', () => {
|
||||||
|
for (const source of [
|
||||||
|
fixtureSource({ repository: 123 }),
|
||||||
|
fixtureSource({ revision: 123 }),
|
||||||
|
fixtureSource({ runId: 123456 }),
|
||||||
|
]) {
|
||||||
|
assert.throws(
|
||||||
|
() => normalizeSource(source),
|
||||||
|
/source (repository|revision|runId)/,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.throws(
|
||||||
|
() => evidenceDigest(Array.from({ length: 100_000 }, () => null)),
|
||||||
|
/node budget exceeded/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('CLI merges, audits and refuses overwrite, symlink and source drift', (t) => {
|
||||||
|
const temporaryDirectory = fs.realpathSync(
|
||||||
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-console-capacity-evidence-')),
|
||||||
|
);
|
||||||
|
t.after(() =>
|
||||||
|
fs.rmSync(temporaryDirectory, { recursive: true, force: true }),
|
||||||
|
);
|
||||||
|
const source = fixtureSource();
|
||||||
|
const x64Path = path.join(temporaryDirectory, 'x64.json');
|
||||||
|
const arm64Path = path.join(temporaryDirectory, 'arm64.json');
|
||||||
|
const outputPath = path.join(temporaryDirectory, 'cross.json');
|
||||||
|
writeJson(x64Path, fixtureArchitecture('x64', source));
|
||||||
|
writeJson(arm64Path, fixtureArchitecture('arm64', source));
|
||||||
|
|
||||||
|
const merged = runCli([
|
||||||
|
'--mode=merge',
|
||||||
|
...sourceArguments(source),
|
||||||
|
`--x64=${x64Path}`,
|
||||||
|
`--arm64=${arm64Path}`,
|
||||||
|
`--output=${outputPath}`,
|
||||||
|
]);
|
||||||
|
assert.equal(merged.status, 0, merged.stderr);
|
||||||
|
assert.equal(fs.statSync(outputPath).mode & 0o777, 0o600);
|
||||||
|
const report = readJsonFile(outputPath, 'release evidence');
|
||||||
|
validateReleaseEvidence(report, source);
|
||||||
|
|
||||||
|
const audit = runCli([
|
||||||
|
'--mode=audit',
|
||||||
|
...sourceArguments(source),
|
||||||
|
`--report=${outputPath}`,
|
||||||
|
]);
|
||||||
|
assert.equal(audit.status, 0, audit.stderr);
|
||||||
|
assert.equal(JSON.parse(audit.stdout).passed, true);
|
||||||
|
|
||||||
|
const overwrite = runCli([
|
||||||
|
'--mode=merge',
|
||||||
|
...sourceArguments(source),
|
||||||
|
`--x64=${x64Path}`,
|
||||||
|
`--arm64=${arm64Path}`,
|
||||||
|
`--output=${outputPath}`,
|
||||||
|
]);
|
||||||
|
assert.notEqual(overwrite.status, 0);
|
||||||
|
assert.match(overwrite.stderr, /output must be a new private file/);
|
||||||
|
|
||||||
|
const drift = runCli([
|
||||||
|
'--mode=audit',
|
||||||
|
...sourceArguments(fixtureSource({ revision: 'b'.repeat(40) })),
|
||||||
|
`--report=${outputPath}`,
|
||||||
|
]);
|
||||||
|
assert.notEqual(drift.status, 0);
|
||||||
|
assert.match(drift.stderr, /belongs to another source/);
|
||||||
|
|
||||||
|
const linkPath = path.join(temporaryDirectory, 'report-link.json');
|
||||||
|
fs.symlinkSync(outputPath, linkPath);
|
||||||
|
const symlink = runCli([
|
||||||
|
'--mode=audit',
|
||||||
|
...sourceArguments(source),
|
||||||
|
`--report=${linkPath}`,
|
||||||
|
]);
|
||||||
|
assert.notEqual(symlink.status, 0);
|
||||||
|
assert.match(symlink.stderr, /readable non-symlink file/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('capture fails closed before Docker without explicit live opt-in', () => {
|
||||||
|
const result = runCli([
|
||||||
|
'--mode=capture',
|
||||||
|
...sourceArguments(),
|
||||||
|
`--architecture=${process.arch === 'arm64' ? 'arm64' : 'x64'}`,
|
||||||
|
'--image=qinglong3-cluster-admin:ci-test',
|
||||||
|
'--output=/tmp/ql3-console-capacity-should-not-exist.json',
|
||||||
|
]);
|
||||||
|
assert.notEqual(result.status, 0);
|
||||||
|
assert.match(
|
||||||
|
result.stderr,
|
||||||
|
/QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE=1 is required/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('live source freezes native cgroup v2, isolation and assertion rotation mechanics', () => {
|
||||||
|
for (const contract of [
|
||||||
|
"process.platform !== 'linux' || process.arch !== architecture",
|
||||||
|
'process.version !== NODE_VERSION',
|
||||||
|
"'--memory',\n '192m'",
|
||||||
|
"'--memory-swap',\n '192m'",
|
||||||
|
"'--cpus',\n '0.25'",
|
||||||
|
"'--pids-limit',\n String(PIDS_MAX)",
|
||||||
|
"'--read-only'",
|
||||||
|
"'--cap-drop',\n 'ALL'",
|
||||||
|
"'--security-opt',\n 'no-new-privileges'",
|
||||||
|
'`127.0.0.1:${port}:${port}/tcp`',
|
||||||
|
"memoryPeakBytes: integer('memory.peak')",
|
||||||
|
'memoryEvents: { low: events.low',
|
||||||
|
"swapMaxBytes: integer('memory.swap.max')",
|
||||||
|
'requestCert: true',
|
||||||
|
'rejectUnauthorized: true',
|
||||||
|
"minVersion: 'TLSv1.3'",
|
||||||
|
"fs.renameSync(next, '/authority/assertion.jwt')",
|
||||||
|
"operation: 'run.cancellation.summary'",
|
||||||
|
'consoleRestarted: false',
|
||||||
|
'mutation: false',
|
||||||
|
'created.network = true',
|
||||||
|
"cleanupDocker(['network', 'rm', network])",
|
||||||
|
]) {
|
||||||
|
assert.ok(scriptSource.includes(contract), `missing ${contract}`);
|
||||||
|
}
|
||||||
|
assert.doesNotMatch(scriptSource, /run\.cancellation\.(?:rearm|stop|retry)/);
|
||||||
|
assert.doesNotMatch(scriptSource, /--privileged|--network[= ]host/);
|
||||||
|
});
|
||||||
@@ -30,6 +30,14 @@ test('accepts the reviewed native CI and digest release contracts', () => {
|
|||||||
clusterAdminOperatorContext: true,
|
clusterAdminOperatorContext: true,
|
||||||
clusterAdminContextPreflight: true,
|
clusterAdminContextPreflight: true,
|
||||||
clusterAdminContextReadiness: true,
|
clusterAdminContextReadiness: true,
|
||||||
|
clusterCopilotConsoleCapacityEvidence: {
|
||||||
|
nativeArchitectures: ['x64', 'arm64'],
|
||||||
|
memoryLimitMiB: 192,
|
||||||
|
minimumHeadroomMiB: 32,
|
||||||
|
assertionRotation: true,
|
||||||
|
assertionExpiryRejected: true,
|
||||||
|
sourceBound: true,
|
||||||
|
},
|
||||||
releaseVersionAudit: true,
|
releaseVersionAudit: true,
|
||||||
deploymentLockMaterialization: true,
|
deploymentLockMaterialization: true,
|
||||||
ociAttestations: true,
|
ociAttestations: true,
|
||||||
@@ -315,6 +323,50 @@ test('rejects removal of the native Cluster Admin product facade gate', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects a Console capacity capture without the exact native live opt-in', () => {
|
||||||
|
const mutated = ciSource.replace(
|
||||||
|
"QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '1'",
|
||||||
|
"QL3_CLUSTER_COPILOT_CONSOLE_CAPACITY_LIVE: '0'",
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => auditClusterImageCiWorkflow(mutated),
|
||||||
|
/exact source-bound Console capacity envelope/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects removal of the Console capacity evidence protocol tests', () => {
|
||||||
|
const mutated = ciSource.replace(
|
||||||
|
'test/back/ql3ClusterCopilotConsoleCapacityEvidence.test.cjs',
|
||||||
|
'test/back/ql3ClusterCopilotConsoleCapacityEvidence.removed.cjs',
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => auditClusterImageCiWorkflow(mutated),
|
||||||
|
/Console capacity evidence protocol tests/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects Console capacity evidence that is not gated by the native image matrix', () => {
|
||||||
|
const mutated = ciSource.replace(
|
||||||
|
'cluster-console-capacity-release-evidence:\n name: Cross-architecture Cluster Copilot Console capacity evidence\n needs: cluster-image',
|
||||||
|
'cluster-console-capacity-release-evidence:\n name: Cross-architecture Cluster Copilot Console capacity evidence\n needs: image-oci',
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => auditClusterImageCiWorkflow(mutated),
|
||||||
|
/exact native x64 and arm64 reports/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects removal of the offline Console capacity release audit', () => {
|
||||||
|
const mutated = ciSource.replace(
|
||||||
|
' --mode=audit \\\n',
|
||||||
|
' --mode=merge \\\n',
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => auditClusterImageCiWorkflow(mutated),
|
||||||
|
/exact native x64 and arm64 reports/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects a Cluster Admin live gate that omits operator context injection', () => {
|
test('rejects a Cluster Admin live gate that omits operator context injection', () => {
|
||||||
const contract = fs.readFileSync(
|
const contract = fs.readFileSync(
|
||||||
path.join(root, 'scripts/ql3-cluster-admin-product-live-contract.cjs'),
|
path.join(root, 'scripts/ql3-cluster-admin-product-live-contract.cjs'),
|
||||||
|
|||||||
Reference in New Issue
Block a user