mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): fence cluster deployment transitions
This commit is contained in:
@@ -11,6 +11,29 @@
|
||||
|
||||
最新增量证据(2026-08-16):
|
||||
|
||||
- D-342/ADR-0434(已接受;资源退休 ceremony 与真实公开 catalog 运行仍待后续 Gate):Cluster deployment command、preflight 与
|
||||
receipt 在尚未发布前直接升为 v2,并以目标 namespace 中唯一 `qinglong3-deployment-head` ConfigMap 建立部署顺序 authority。每次
|
||||
`install|upgrade|rollback` 都必须绑定 exact expected Head generation/deployment/lock/state digest;preflight 读取当前 Head,apply
|
||||
再用 API Server 返回的 opaque `resourceVersion` 执行 create/replace CAS,先取得 `applying`,完成五步 server-side apply/convergence
|
||||
后再提交 `committed`。相同意图可从 applying 或已提交但丢失本地 receipt 的窗口确定性恢复,陈旧 preflight、并发不同意图、Head
|
||||
漂移与本地 receipt 对已前进 Head 的重放全部失败关闭。locked manifest 必须完整覆盖 `control|control-ai|admin|worker`,自动推导并
|
||||
排序唯一 resource inventory;upgrade 不能省略现存对象,rollback 必须与上一 deployment 使用相同 inventory,避免把普通 apply 不会
|
||||
删除的残留对象冒充成功。删除仍保留给后续绑定 UID/resourceVersion 的独立 retirement ceremony,不采用 alpha prune。Head 只增加一个
|
||||
小 ConfigMap 和 preflight 一次 GET、apply 两次 CAS;没有 controller、CRD、webhook、Pod、listener、timer、数据库、migration、Pool、
|
||||
新 package 或第三方依赖,Local/Edge/Standalone 零导入、零制品增量。Service Bridge 审计同时发现其仅为错误类型而加载完整 foundation
|
||||
contract;已把同一 class identity 下沉到既有 `local-owner-cli/deployment/foundation/error` 内部模块,各 importer 改用叶子入口,未拆出
|
||||
浅 package。定向 deployment ceremony 14/14、Local Owner 171 项为 166 pass/5 条件 skip、完整 backend 1,331 项为 1,329 pass/2 条件
|
||||
skip/0 fail、18-package clean build/test 退出 0。10 项架构/部署审计与 14 档 Local artifact 全部 compatible,package boundary 仍为
|
||||
18 packages、`singleSourcePackages=[]`、`shallowSourcePackages=[]`;Local Owner 的 107 个源码中 106 个位于领域目录。默认
|
||||
Edge/Standalone 保持 2,589,890/2,589,968 bytes,MCP 保持 7,315,930/7,316,038 bytes;Cluster Admin dry-run pack 保持 250 files、
|
||||
271,238-byte tarball、1,690,196-byte unpacked。隔离三节点 K3s `v1.34.3+k3s1`/arm64 真实运行 6 个 release resource、4 个零副本
|
||||
Deployment 和 generation 1 committed Head,deployment/preflight/receipt digest 分别为
|
||||
`sha256:f911cda19195735d00fcc6317db2c22794855e5eb5795696b19a4239fb68a263`、
|
||||
`sha256:574f80dde330c5b4fe41c681906a67ffd6e4b608dedc6d955574771b2bd5c711`、
|
||||
`sha256:0f2edd2037a4df7ae6eb28616aa38a280498660443d0f091e1fac5c1ff532258`,资源清理完成。经允许重跑 PostgreSQL 18.6
|
||||
arm64 physical HA,142/142、timeline `1→2`,报告 SHA-256
|
||||
`8ba0b130100b48e19c602546b63844903e567d6997630f8c950b51cf41ee70ea`,独立证据审计通过且容器、卷、网络零残留。live 使用 synthetic
|
||||
lock,只证明部署状态机和目标 API CAS,不冒充尚未产生的公开 GHCR catalog。
|
||||
- D-341/ADR-0433(已接受;真实公开 catalog 运行待实际 release tag):Cluster 最后一跳不再交给裸
|
||||
`kubectl apply -f locked.yaml`。新增工作站级 `cluster.deployment.preflight|apply|receipt.audit`,从 owner-private canonical
|
||||
command 出发独立复验 Kubernetes v2 lock/report、locked manifest、release-set/catalog/workflow/image/annotation 闭包,并固定
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# ADR-0434:Resource-inventory-closed Kubernetes Deployment Head
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-16
|
||||
- 关联 RFC:QL-RFC-0001 D-03、D-14、D-341、D-342
|
||||
- 关联 ADR:ADR-0431、ADR-0433
|
||||
- 修正:ADR-0433 第 7、8 项的无目标状态重放与人工 roll-forward 边界
|
||||
|
||||
## 上下文
|
||||
|
||||
ADR-0433 把 catalog-bound lock、目标 cluster 与 pinned kubectl 绑定到最后一跳,但固定 field manager 的 server-side apply 只解决字段
|
||||
ownership,不提供部署意图的先后顺序 CAS。两个运维者从同一旧状态取得 preflight 后,后执行者仍可能覆盖先执行者;manifest 中移除的
|
||||
对象也不会被普通 apply 删除,若只检查新 manifest,旧对象可残留而 receipt 仍显示成功。
|
||||
|
||||
Kubernetes API 的 server-side apply 文档明确说明,它不适合依赖 current value 的条件更新;这种 lost-update 防护应使用包含当前
|
||||
`resourceVersion` 的 update/replace。`kubectl apply --prune` 的 allowlist 仍标记为 alpha,也不能替代显式资源退休协议。因此部署
|
||||
ceremony 需要一个很小的目标侧 durable head,而不是把并发或删除语义藏进 field ownership。
|
||||
|
||||
## 决策
|
||||
|
||||
1. 尚未发布的 deployment command/preflight/receipt 直接升级为 v2,不保留可绕过 Head 的 v1 双轨。每个 preflight/apply 必须显式提供
|
||||
`transitionKind=install|upgrade|rollback` 和 exact `expectedHead={generation,deploymentDigest,lockDigest,stateDigest}`。
|
||||
2. Cluster 生产转换只接受 `control,control-ai,admin,worker` 完整 role surface。locked manifest 推导唯一 control namespace,并生成按
|
||||
`apiVersion/kind/namespace/name` 排序、去重的完整 resource inventory;调用方不能另选 Head namespace 或提交手写 inventory。
|
||||
3. 目标 namespace 中固定使用一个 `qinglong3-deployment-head` ConfigMap,唯一数据键为 `head.json`。它保存 schema、phase、generation、
|
||||
exact transition、当前/前一 lock 与 inventory 摘要、target/tool authority、五项 workload step transcript digest、deployment digest 和
|
||||
self digest;不保存 manifest 正文、token、credential、kubeconfig 或命令输出正文。
|
||||
4. preflight 在 identity read 后读取 Head,要求它为空或 `committed`,并比较 exact expected Head。第一次 `install` 只接受空 Head;
|
||||
`upgrade` 只接受严格递增 SemVer;`rollback` 只接受 Head 中精确的上一 deployment。
|
||||
5. apply 重读 Head 后,以 ConfigMap create 或带 GET 所得 opaque `resourceVersion` 的 replace 原子取得 `phase=applying`。相同
|
||||
command/mutation/preflight 可以恢复同一 applying intent;任何其他意图、陈旧 preflight、resourceVersion conflict 或 Head 漂移都在
|
||||
workload mutation 前失败关闭。
|
||||
6. server-side apply、convergence read 与末次 cluster identity 全部成功后,ceremony 再以 applying ConfigMap 的 resourceVersion replace
|
||||
为 `phase=committed`,然后才 no-replace 发布 receipt。若 committed response 已返回而本地 receipt 丢失,相同 command 可从 Head
|
||||
确定性重建;已有本地 receipt 的重放也必须联网确认 Head 尚未前进。
|
||||
7. `upgrade` 要求 active inventory 是 target inventory 的子集;省略现存对象立即失败,不能隐式 prune。`rollback` 额外要求 current、
|
||||
target 与 previous inventory 完全相同。需要删除对象时等待独立的 UID/resourceVersion delete-precondition retirement ceremony;本
|
||||
ADR 不把“对象仍残留”冒充成功回退。
|
||||
8. receipt 继续声明 Kubernetes 多资源 mutation 非事务,但新增 Head generation/deployment/state digest、完整 inventory、
|
||||
`deploymentHeadCas=true`、`resourceInventoryClosed=true` 与
|
||||
`recovery=resume_exact_transition_from_target_head`。offline receipt audit 不访问 API Server,也不宣称 Head 仍是当前状态。
|
||||
|
||||
## 部署与资源影响
|
||||
|
||||
- Local/Edge/Standalone 路径零导入、零制品增量;路由设备不创建 Head、不加载 Kubernetes/YAML/semver 或 Cluster role 代码。
|
||||
- Cluster 每个目标 namespace 只增加一个小型 ConfigMap;无 controller、CRD、webhook、ServiceAccount、Pod、listener、timer、watcher、
|
||||
数据库、migration、SQL 或 Pool。preflight 比 ADR-0433 多一次 Head GET;首次 apply 多一次 create,后续 apply 为 acquire replace 和
|
||||
commit replace。
|
||||
- 逻辑继续内聚在根级工作站 ceremony,不新增浅 workspace package。Head 是发布排序证据,不是通用 desired-state controller。
|
||||
- namespace 必须先由受控 bootstrap 创建;否则 Head create 与 namespaced workload dry-run 都自然失败关闭。
|
||||
|
||||
## 被拒绝的替代方案
|
||||
|
||||
### 仅依赖相同 server-side apply field manager
|
||||
|
||||
拒绝。field manager 管理字段 ownership,不比较调用者观察到的上一部署 generation,陈旧 writer 仍可成为最后写入者。
|
||||
|
||||
### 使用 `kubectl apply --prune`
|
||||
|
||||
拒绝。当前 prune allowlist/ApplySet 仍有 alpha 边界,而且删除必须绑定已观察对象 UID/resourceVersion 与独立 receipt;隐式集合删除无法满足
|
||||
这一证明要求。
|
||||
|
||||
### 在 PostgreSQL 或新 controller 中保存部署锁
|
||||
|
||||
拒绝。最后一跳的并发事实应与目标 API Server 同故障域;额外数据库或常驻 reconciler 会扩大低资源、凭据、升级与可用性边界。
|
||||
|
||||
## 验证
|
||||
|
||||
- 定向 Node 契约 14/14,通过空 Head install、create/replace CAS、committed receipt、同意图 applying 恢复、陈旧 preflight 阻断、
|
||||
resource omission fail-closed、错误/漂移不发布 receipt、closed command 与低敏 CLI;
|
||||
- 隔离三节点 K3s `v1.34.3+k3s1`/Linux arm64 真实运行 6 个 release resource、4 个零副本 Deployment 和一个固定 Head ConfigMap;
|
||||
Head 从 absent 经 applying 到 generation 1 committed,deployment digest 为
|
||||
`sha256:f911cda19195735d00fcc6317db2c22794855e5eb5795696b19a4239fb68a263`,resource inventory 为 6,receipt audit 通过,临时
|
||||
container/network 清理完成;
|
||||
- 完整 backend、18-package、边界、制品与 PostgreSQL HA 结果记录在 QL-RFC-0001 D-342。
|
||||
|
||||
## 规范依据
|
||||
|
||||
- [Kubernetes API concepts:resourceVersion 与 conditional update](https://kubernetes.io/docs/reference/using-api/api-concepts/)
|
||||
- [Kubernetes declarative object management:prune/ApplySet 状态](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/)
|
||||
@@ -437,6 +437,7 @@
|
||||
| [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 |
|
||||
| [ADR-0434](./ADR-0434-resource-inventory-closed-kubernetes-deployment-head.md) | Resource-inventory-closed Kubernetes Deployment Head | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -194,13 +194,16 @@ kubectl --kubeconfig="${kubeconfig}" --context="${context}" \
|
||||
ceremony 目录必须为当前 UID 的 canonical `0700` 目录;command、locked manifest、lock report、kubeconfig、preflight 与 receipt
|
||||
都必须是单链接 `0600` 文件。kubectl 使用 realpath 后的 absolute executable,并记录其 SHA-256。kubeconfig 禁止 `exec` 与
|
||||
`auth-provider`。每个 kubectl 调用使用独立临时 HOME/XDG cache/TMPDIR,结束即清理,不读取 ambient HOME 或在当前目录创建
|
||||
`.kube/cache`。以下是 preflight command 的逻辑结构;实际文件必须用 `JSON.stringify(value) + "\n"` 写成单行 canonical JSON,
|
||||
并以 `0600` no-replace 创建:
|
||||
`.kube/cache`。目标 Namespace 还承载固定名称 `qinglong3-deployment-head` 的小型 ConfigMap。它不属于应用 manifest,也不由
|
||||
server-side apply 接管;ceremony 只通过 API Server 返回的 opaque `resourceVersion` 执行 create/replace CAS。第一次安装使用
|
||||
`install` 和空 Head;后续 `upgrade`/`rollback` 必须从上一份 receipt 的 `deploymentHead` 复制 generation、deployment/lock/state
|
||||
digest,不能自行推测。以下是第一次安装的 preflight command 逻辑结构;实际文件必须用 `JSON.stringify(value) + "\n"` 写成单行
|
||||
canonical JSON,并以 `0600` no-replace 创建:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"schema": "qinglong/kubernetes-deployment-command@v1",
|
||||
"schema": "qinglong/kubernetes-deployment-command@v2",
|
||||
"operation": "cluster.deployment.preflight",
|
||||
"request": {
|
||||
"preflightId": "<new UUID>",
|
||||
@@ -222,6 +225,13 @@ ceremony 目录必须为当前 UID 的 canonical `0700` 目录;command、locke
|
||||
},
|
||||
"context": "<explicit context>",
|
||||
"expectedClusterUid": "<reviewed kube-system UID>",
|
||||
"transitionKind": "install",
|
||||
"expectedHead": {
|
||||
"generation": 0,
|
||||
"deploymentDigest": null,
|
||||
"lockDigest": null,
|
||||
"stateDigest": null
|
||||
},
|
||||
"output": "<unused canonical absolute preflight.json>"
|
||||
}
|
||||
}
|
||||
@@ -233,13 +243,14 @@ ceremony 目录必须为当前 UID 的 canonical `0700` 目录;command、locke
|
||||
pnpm cluster-deployment:ql3 -- --command-file="${preflight_command}"
|
||||
```
|
||||
|
||||
成功后,人工核对返回的 `preflightDigest`、lock/catalog digest、cluster UID 和
|
||||
`kubernetesMutation:false`。apply 必须使用新的 mutation UUID,并精确复用所有 target/input authority:
|
||||
成功后,人工核对返回的 `preflightDigest`、lock/catalog digest、cluster UID、完整 resource inventory、`deploymentHead` 和
|
||||
`kubernetesMutation:false`。apply 必须使用新的 mutation UUID,并精确复用 transition、expected Head 与所有 target/input
|
||||
authority:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"schema": "qinglong/kubernetes-deployment-command@v1",
|
||||
"schema": "qinglong/kubernetes-deployment-command@v2",
|
||||
"operation": "cluster.deployment.apply",
|
||||
"request": {
|
||||
"mutationId": "<new UUID>",
|
||||
@@ -265,22 +276,36 @@ pnpm cluster-deployment:ql3 -- --command-file="${preflight_command}"
|
||||
},
|
||||
"context": "<same context>",
|
||||
"expectedClusterUid": "<same cluster UID>",
|
||||
"transitionKind": "install",
|
||||
"expectedHead": {
|
||||
"generation": 0,
|
||||
"deploymentDigest": null,
|
||||
"lockDigest": null,
|
||||
"stateDigest": null
|
||||
},
|
||||
"output": "<unused canonical absolute receipt.json>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
同样通过 `cluster-deployment:ql3` 执行。apply 会重新完成 lock 检查、cluster identity 和 server-side dry-run,然后以固定
|
||||
`qinglong3-catalog-lock` field manager 执行 server-side apply,不使用 `--force-conflicts`;最后读取 live objects,验证
|
||||
同样通过 `cluster-deployment:ql3` 执行。apply 先重读 Head 并以 create/resourceVersion replace 把唯一意图置为 `applying`,再重新
|
||||
完成 lock 检查、cluster identity 和 server-side dry-run,然后以固定 `qinglong3-catalog-lock` field manager 执行 server-side
|
||||
apply,不使用 `--force-conflicts`;最后读取 live objects,验证
|
||||
UID/resourceVersion、完整期望字段、四类 immutable image/catalog annotations 和受影响资源的 managed-field ownership,再次确认
|
||||
cluster UID 后才发布 receipt。
|
||||
cluster UID,最后以同一 ConfigMap 的新 resourceVersion 提交 `committed` Head 后才发布 receipt。Head 保存当前与前一 deployment
|
||||
摘要、完整有序 resource inventory、五个 workload step digest 和 self digest;它不保存 credential、token 或 manifest 正文。
|
||||
|
||||
`upgrade` 只接受严格递增 SemVer,且 active inventory 必须是目标 inventory 的子集;遗漏对象不会借助隐式 prune 删除,而是在
|
||||
mutation 前失败关闭。`rollback` 只接受 Head 中精确的上一部署 lock,且当前/目标 inventory 必须完全相同;涉及资源退休时须等待
|
||||
独立的 UID/resourceVersion-precondition retirement ceremony,不能把遗留对象伪装成回滚成功。
|
||||
|
||||
离线审计使用 `cluster.deployment.receipt.audit` command,其中 `applyCommand.expectedDigest` 是 apply command 文件完整字节的
|
||||
SHA-256,`receipt.expectedDigest` 是 receipt 内的 `receiptDigest`。审计不会访问 Kubernetes API,结果必须保持
|
||||
`externalResultsReplayed:false`、`kubernetesMutation:false`。
|
||||
|
||||
多资源 apply 不是事务,也不提供自动删除式 rollback。失败或 receipt 响应丢失时保留原文件:同一 command/field manager 可安全
|
||||
重放并重新做 live convergence;需要回退时,以上一份 catalog-bound lock 创建新的显式 preflight/apply,而不是修改旧 receipt。
|
||||
多资源 apply 不是事务,也不提供自动删除式 rollback。失败或 receipt 响应丢失时保留原文件:只有相同 command/mutation/preflight
|
||||
意图能从 `applying` Head 重做 live convergence;不同意图和陈旧 preflight 都失败关闭。若 Head 已 `committed` 但本地 receipt 丢失,
|
||||
同一 command 可从目标 Head 确定性重建 receipt。已有本地 receipt 的重放也会联网确认 Head 尚未前进,不能离线冒充当前部署。
|
||||
|
||||
## 准入检查
|
||||
|
||||
|
||||
Reference in New Issue
Block a user