feat(ql3): add catalog-bound cluster deployment ceremony

This commit is contained in:
whyour
2026-08-16 17:12:37 +08:00
parent dce72c800f
commit abf2d25d63
9 changed files with 3030 additions and 4 deletions
@@ -0,0 +1,86 @@
# ADR-0433Catalog-bound Kubernetes 目标部署 Ceremony
- 状态:Accepted
- 日期:2026-08-16
- 关联 RFCQL-RFC-0001 D-03、D-14、D-337、D-339、D-341
- 关联 ADRADR-0429、ADR-0431、ADR-0432
## 上下文
ADR-0431 已把 verified release catalog 物化为 Kubernetes v2 locked manifest/report,但最后一步仍由运维者直接执行
`kubectl apply -f locked.yaml`。该命令没有再次绑定 report self-digest、目标 cluster、kubeconfig、kubectl executable 或
field manager;检查过的文件、实际输入和实际 API target 之间仍存在复制、context 漂移与 TOCTOU 窗口。成功输出也没有 durable
receipt,响应丢失后只能凭人工判断是否重放。
Cluster 需要解决这个缺口,但不能把 Kubernetes、YAML 或 registry 工具带到 Local/Edge,也不能为一次发布新增常驻 controller、
webhook 或长期 ServiceAccount authority。
## 决策
1. 在现有维护工作站脚本边界增加 `cluster.deployment.preflight``cluster.deployment.apply`
`cluster.deployment.receipt.audit` 三种 canonical command;只提供一个
`pnpm cluster-deployment:ql3 -- --command-file=/absolute/private/command.json` 入口,不新增 workspace package 或生产依赖。
2. command、locked manifest、lock report、kubeconfig、preflight 与 receipt 必须位于 current-UID 的 canonical `0700` 目录,文件为
canonical、current-UID、单链接 `0600` regular file。所有输入使用 `O_NOFOLLOW|O_CLOEXEC` stable descriptor 有界读取,调用前后
复验 dev/inode/size/mtime/ctime 与 SHA-256。kubectl 必须是 absolute canonical、current-UID 或 root owner、不可 group/other write
的单链接 executable,并由 command 固定其 SHA-256。
3. 目标 consumer 独立验证 `qinglong/kubernetes-deployment-lock@v2` exact shape/self-digest、3.x release identity、catalog workflow/
immutable reference/release-set 闭包、required role 顺序、全部 GHCR digest reference、manifest byte digest、资源/authority 数量、
五项 release annotation,以及未知或畸形 QingLong image authority 为零。不能只相信 materializer 的 success stdout。
4. kubeconfig 必须由 command 固定 SHA-256 和 explicit context;禁止 `exec` 与 legacy `auth-provider`,避免稳定文件读取后再隐式执行
ambient credential plugin。每次网络动作前显式读取 `kube-system` Namespace UID,并与人工审核的 `expectedClusterUid` 精确匹配。
5. preflight 只执行固定 manager `qinglong3-catalog-lock``kubectl apply --server-side --dry-run=server --validate=strict -f=-`,通过
stdin 发送已验证的内存字节,不让 kubectl 按路径二次读取 manifest。所有承载 QingLong image authority 的资源必须显式携带
namespace,禁止依赖 context 的 ambient default namespace。成功后才 no-replace 发布 self-digest preflight report;它明确
`networkAccess=true``kubernetesMutation=false`。目标 namespace 必须预先存在,否则包含 namespaced resources 的 dry-run 自然失败关闭。
6. apply 必须消费 exact preflight digest,并重新执行全部离线检查、cluster UID 与 server-side dry-run;不使用
`--force-conflicts`。实际 apply 后以 `kubectl get -f=- -o=json --show-managed-fields=true` 读取同一资源集合:每个对象必须有
UID/resourceVersion,全部期望字段递归匹配;承载 QingLong image authority 的资源必须由固定 field manager 以 `Apply` 持有,所有
image/catalog annotation 再次精确验证。结束时再次检查 cluster UID 和全部稳定文件。
7. 只有所有步骤成功后才以 `0600` no-replace 发布 self-digest receipt。receipt 绑定 mutation、command、preflight、lock/manifest/
catalog digest、cluster UID、kubeconfig/kubectl digest、各 argv/stdout/stderr digest 与 byte count。已有 exact receipt 的相同 command
离线返回;apply 成功但 receipt 丢失时,使用同一 command/field manager 重放 server-side apply,再以 live convergence 收敛。
8. Kubernetes 多资源 apply 不是事务,receipt 必须固定 `crossResourceAtomicity=false`。失败时不自动删除或回滚资源;先审计 live state,
再用上一份 catalog-bound lock 执行新的显式 roll-forward。offline receipt audit 只重建 canonical/self/command binding,明确
`externalResultsReplayed=false`,不伪称离线重放 API Server 结果。
## 部署与资源影响
- Local/Edge/Standalone 零导入、零制品增量、零常驻 CPU/RSS/网络/写放大;低配路由器继续只消费 Local v2 selection。
- Cluster 不新增 controller、webhook、CRD、RBAC、ServiceAccount、Pod、listener、timer、watcher、数据库、migration、SQL 或 Pool。
ceremony 使用部署者原有 kubeconfig 权限并在命令结束后退出。每个 kubectl 子进程使用独立 `0700` 临时 HOME/XDG cache/TMPDIR
结束即清理,不读取 ambient HOME,也不在仓库或 operator home 留下 discovery cache。
- 实现内聚在 `scripts/lib/ql3-kubernetes-deployment-ceremony.cjs` 与薄 CLI;没有为单一工作站流程拆出浅 workspace package。
- 每次 preflight 为一次 cluster identity read 和一次 server-side dry-runapply 为 identity read、dry-run、apply、convergence read、末次
identity read。manifest/report/kubeconfig 各有明确 byte ceilingprocess stdout/stderr 各最多 4 MiB。
## 被拒绝的替代方案
### 继续文档化裸 `kubectl apply`
拒绝。它无法证明 apply 的文件、report、context、cluster 和 executable 与人工审核对象相同,也没有 response-loss recovery receipt。
### 使用 `kubectl diff` 证明收敛
拒绝。真实 K3s 门证明 kubectl 会隐式从 `$PATH` 启动外部 `diff`,扩大未固定的 executable authority。受审 convergence read 只调用
同一 pinned kubectl,并直接验证 live object identity、managed field ownership 和完整期望字段。
### 自动 `--force-conflicts` 或失败后删除资源
拒绝。强夺其他 field manager 或跨资源猜测回滚会扩大故障;冲突必须失败关闭,多资源非原子事实必须进入 receipt 与人工恢复流程。
### 在集群内新增持续部署 controller
拒绝。当前缺口是显式 release rollout 的最后一跳,不值得新增常驻 availability、credential、certificate 与升级故障域。
## 验证
- 定向契约 10/10,覆盖 lock/report/manifest/annotation、cluster UID、文件权限、kubeconfig executable auth、server dry-run、apply、live
convergence、field manager、receipt 重签、response replay、closed CLI 与低敏失败;
- 隔离三节点 K3s `v1.34.3+k3s1`/Linux arm64 真实运行 6 个资源、4 个零副本 Deploymentpreflight、server-side apply、live
convergence 与 offline receipt audit 全部通过,固定 manager 为 `qinglong3-catalog-lock`,临时 Docker container/network 零残留;
- 最终 live cluster UID 为 `7b2a5391-41a3-4905-90cc-3b831bef0058`preflight digest 为
`sha256:7c6db6236a704cd7791f94268b3883d6385a75a993a14d1611791f885fb65404`receipt digest 为
`sha256:58817a667bccf28c068fff40619cf13d2d0a1be84153de66a137b005feb536ba`。该 synthetic lock 只验证目标 apply 语义,不冒充公开
GHCR catalog ceremony
- 完整 backend、18-package、边界审计与制品结果记录在 QL-RFC-0001 D-341。
+1
View File
@@ -436,6 +436,7 @@
| [ADR-0430](./ADR-0430-auditable-release-catalog-consumption-ceremony.md) | 可审计的 Release Catalog 消费工作站 Ceremony | Accepted(真实公开 catalog 运行待实际 release tag |
| [ADR-0431](./ADR-0431-catalog-bound-deployment-lock-chain.md) | Catalog-bound Deployment Lock 证据链 | Accepted(真实公开 catalog 运行待实际 release tag |
| [ADR-0432](./ADR-0432-target-side-catalog-bound-local-compose-revisions.md) | 目标侧 Catalog-bound Local Compose 修订 | Accepted |
| [ADR-0433](./ADR-0433-catalog-bound-kubernetes-target-deployment-ceremony.md) | Catalog-bound Kubernetes 目标部署 Ceremony | Accepted |
## 规则