feat(ql3): bind offline adopted target images

This commit is contained in:
whyour
2026-08-30 19:50:24 +08:00
parent 38d85952e7
commit 0f4b6b43fb
15 changed files with 345 additions and 23 deletions
+2
View File
@@ -1,5 +1,7 @@
# QingLong 3.0 Architecture RFC
- D-426b1/ADR-0522(源码候选,尚未形成新阶段实物):D-426a 暴露出真实的离线切换缺口:Trial Kit archive 只有本地 image reference 与 Docker content ID,既有 target barrier 却只接受 registry `name@sha256:...`;伪造 GHCR RepoDigest 或 Alpha release catalog 会污染正式发布权威。现统一引入 `{authority,reference,imageId}` target image identity,正式部署保持 `registry-digest`,离线 Trial Kit 使用 `local-image-id`,两者都同时核对容器 `Config.Image``.Image` content ID,并把三项绑定进 cutover journal。adopted bundle 新增 `docker-target` service kind,生成 Application v4 与内容绑定的 `service/docker-target.json`,固定 numeric UID:GID、restart=no、network none、read-only rootfs、drop ALL、no-new-privileges、Profile memory/PID 和 exact read-write/read-only mountsdescriptor 不创建或启动容器。没有新增 package、依赖或稳态进程。该切片只有定向源码测试证据,不能替代已交付的 D-426a 双架构 artifact;必须等 D-426b2 在 exact 上传 bundle 上实跑受认证 transform/apply、target start/stop 和 clean `rollback_candidate`,才允许宣称新阶段产物。
- D-426a/ADR-0521(已交付同源双架构 headless Alpha 阶段实物):二十天研发的阶段产物继续从“只读盘点”推进到仍不触碰生产切换的 side-by-side 暂存。Trial Kit 新增 canonical `upgrade-rehearsal.sh`,只接受操作者从 D-425 完整 evidence 中审核并显式提交的 SQLite/data-directory plan digest;它核对整包与 exact Operator identity 后,在新的 `0700/0600` rehearsal root 内以 read-only legacy bind、无网络、只读 rootfs、drop-all、128 MiB/0.5 CPU/32 PID 顺序执行 SQLite stage/verify/activation 和完整目录 stage/verify。`stage-summary.json` 绑定 source/architecture、两个 reviewed plan、SQLite manifest/activation 与目录 manifest digest,并固定 `legacySource=read_only``cutover=not_authorized`。Trial Kit/verification/auditor 升为 `@v7/@v5/@v4`Local milestone 升为 `@v4` 并绑定双架构 `upgradeRehearsalSha256`;显式 artifact job 必须在将要上传的 exact bundle 上从 readiness plan 接续实跑 rehearsal。提交 `7a8acacb6cb49bda2116bf029fbbfe447ae5d911` 的普通 CI [run 33306005705](https://github.com/whyour/qinglong/actions/runs/33306005705) 为 41 success/3 expected scope skip/0 fail,同源 Kubernetes deployment [run 33306005706](https://github.com/whyour/qinglong/actions/runs/33306005706) 成功;显式 Local headless [run 33306650776](https://github.com/whyour/qinglong/actions/runs/33306650776) 为 42 success/2 Cluster scope skip/0 failexact 双架构 bundle 实跑 readiness 与 rehearsal 后生成 amd64 `187,554,547` bytes、arm64 `184,786,163` bytes 和 `6,206` bytes milestone v4,均保留至 2026-09-29。下载后的 milestone checksum 与离线 auditor 返回 `compatible=true`,并确认两个架构的 rehearsal digest 不同。没有 transform/apply、Owner/Secret authority、2.x stop、3.0 target start、cutover、rollback、migration、依赖、package、daemon、listener、timer 或稳态资源增量;D-426b/c 才分别闭合 adopted start/clean rollback 与 write-after reconciliation。
- D-425/ADR-0520(已交付同源双架构 headless Alpha 阶段实物):二十天研发的 Local 阶段产物开始服务现有 2.x 部署,而不再只支持 fresh 数据根。Trial Kit 新增 manifest/checksum/auditor 共同绑定的 canonical `upgrade-readiness.sh`;它只接受 `edge|standalone`、canonical 2.x data root 和尚不存在的 evidence root,先核对 exact Operator image/source/architecture,再以当前 UID:GID、read-only legacy bind mount、只读 rootfs、`network=none`、drop-all capabilities、128 MiB memory/swap、0.5 CPU、32 PID 运行正式 `local-sqlite.adoption.inspect``local-data-directory.adoption.inspect`。两个完整 plan 写入独立 `0700/0600` evidence,不解析或代替人工审核,不执行 stage、activation、transform/apply、cutover 或 rollback。Trial Kit/verification/auditor 升级为 `@v6/@v4/@v3`Local milestone 升级为 `@v3` 并直接绑定双架构 readiness script digest;显式 artifact job 必须在原生 amd64/arm64 上创建生产形态 2.x fixture,实跑将要上传的 exact 脚本并要求两个结果均为 `inspected`。无新 package、依赖、migration、listener、daemon、timer、watcher 或 Cluster authority;聚焦 bundle/milestone/stage/fixture 回归为 `30/30`。提交 `d6571e4b89eaf29ed6277dd08bbd7ffb57a3705d` 的普通 CI [run 33295923855](https://github.com/whyour/qinglong/actions/runs/33295923855) 为 41 success/3 expected artifact-finalizer skip/0 fail,同源 Kubernetes deployment [run 33295923822](https://github.com/whyour/qinglong/actions/runs/33295923822) 成功;显式 Local headless [run 33300121149](https://github.com/whyour/qinglong/actions/runs/33300121149) 为 42 success/2 scope skip/0 fail,两个原生架构均实跑 exact readiness 并由 bundle auditor 返回 `compatible=true`,下载后的 milestone v3 再确认 source/run/attempt 与两个不同 readiness digest 闭合。amd64/arm64/milestone artifact 保留至 2026-09-29;它们仍为 `alpha_candidate_not_public_release`,该切片也仍只是完整 side-by-side 升级与 clean/write-after 回退 rehearsal 的第一阶段。
@@ -0,0 +1,33 @@
# ADR-0522:内容绑定的离线 Docker Adopted Target
- 状态:Accepted(D-426b 源码候选;阶段实物尚未闭合)
- 日期:2026-08-30
- 决策:D-426b1
## 上下文
D-426a 已交付可下载、可校验、可执行 reviewed side-by-side stage 的双架构 Trial Kit,但其 Docker archive 只含本地 image reference 与 Docker content ID,不具备 registry `RepoDigest`。既有 target-start 契约只接受 `name@sha256:...`,因此下载产物虽然能完成 stage,却不能诚实创建并启动 adopted target。把本地镜像伪装成 GHCR digest,或为 Alpha branch 伪造 release catalog/tag provenance,都会破坏正式 Compose 发布链。
## 决策
目标镜像权威统一为 exact `targetImage`
- `registry-digest`reference 必须是不可变 `name@sha256:...`
- `local-image-id`reference 只允许有界本地 tag,并必须来自同一 Trial Kit
- 两种权威都必须携带 `sha256:...` Docker content ID。
target start/restart/stop 与 Legacy rollback 共享这一不可变对象。容器证据同时核对 `Config.Image` reference 和 `.Image` content IDjournal 中的镜像 digest 绑定 authority/reference/imageId 三者。content ID 漂移不会启动容器,而是收敛到 `manual_required`
adopted bundle 增加 `docker-target` service kind,生成 `service/docker-target.json` 与 Application v4 config。descriptor 固定 numeric UID:GID、`restart=no`、无网络、read-only rootfs、drop ALL、no-new-privileges、Profile memory/PID 上限、deployment root 可写 mount 和 legacy source 只读 mount。它只准备创建材料,不创建或启动容器,也不授予 cutover。
正式 Compose 继续只接受 catalog-bound GHCR digest selectionsystemd/OpenRC 不接收 Docker image authority。没有新增 workspace package、生产依赖、daemon、listener、timer、watcher、队列或稳态资源。
## 阶段实物门
本 ADR 当前只是源码候选,不得称为已交付的 D-426b 阶段实物。只有后续同源原生 amd64/arm64 artifact job 在将要上传的 exact Trial Kit 上完成受认证 transform/apply、生成并验证 `docker-target.json`、真实 target start/stop,并得到 clean `rollback_candidate`,再由 bundle/milestone auditor 闭合,才能把状态升级为“阶段实物已交付”。
在此之前,用户可继续使用 D-426a 的 `upgrade-rehearsal.sh` 完成 read-only legacy + side-by-side stage;不得从本 ADR 推导生产切换授权。
## 后续
D-426b2 接入受认证 transform/apply、descriptor 消费、真实 target start/stop 与 clean rollback candidateD-426c 处理 target 写入后的 `reconciliation_required`。Public prerelease/release 仍需维护者显式授权。
+1
View File
@@ -525,6 +525,7 @@
| [ADR-0519](./ADR-0519-local-console-secret-backed-automation.md) | Local Console Secret-backed 自动化 | AcceptedD-424 双架构 milestone 已交付) |
| [ADR-0520](./ADR-0520-downloadable-local-legacy-upgrade-readiness.md) | 可下载的 Local Legacy 升级就绪盘点 | AcceptedD-425 双架构 Alpha 实物已交付) |
| [ADR-0521](./ADR-0521-reviewed-side-by-side-local-upgrade-stage.md) | 受审核计划驱动的 Local Side-by-side 升级暂存 | AcceptedD-426a 双架构 Alpha 实物已交付) |
| [ADR-0522](./ADR-0522-content-bound-offline-docker-adopted-target.md) | 内容绑定的离线 Docker Adopted Target | AcceptedD-426b 源码候选;阶段实物尚未闭合) |
## 规则
+1
View File
@@ -32,6 +32,7 @@
| D-424 Console Secret-backed 自动化切片 | current-only metadata、强认证 AES-256-GCM create/rotate 与 Task pinned `SecretRef` 绑定已完成;真实 SQLite/loopback、本地与远端门、同源双架构 Console milestone 及离线 auditor 均通过 | Cluster 不复用 Local proof/custody;不提供明文读取、删除或历史浏览;仍不是正式发布或生产升级 |
| D-425 2.x 升级就绪盘点 | 同源 v6 Trial Kit 已交付 amd64/arm64 headless 阶段实物;canonical `upgrade-readiness.sh` 把 2.x root 只读挂载,在 128 MiB/无网络边界内由 exact Operator 生成 SQLite 与完整目录两个计划;artifact job 实跑、bundle auditor 与 milestone v3 均闭合 | 只完成 inspect,不授权 stage、activation、cutover 或 rollback;不是 Public Release |
| D-426a Side-by-side 暂存 | 同源 v7 Trial Kit 已交付 amd64/arm64 headless 阶段实物;reviewed-plan `upgrade-rehearsal.sh` 在新的私有 root 中执行 SQLite stage/verify/activation 与完整目录 stage/verifylegacy root 始终只读,summary 固定 `cutover=not_authorized`exact artifact job 实跑且 milestone v4 离线审计闭合 | 不执行 transform/apply、目标启动、cutover 或回退;仍不是 Public Release |
| D-426b1 离线 adopted target 权威 | 源码候选已支持 registry digest 与 Trial Kit local image ID 两类强绑定,并可生成 Application v4 + `docker-target.json`;正式 Compose catalog 权威未放宽 | 尚未生成新的双架构 artifacttransform/apply、真实 start/stop、clean `rollback_candidate` 等待 D-426b2 exact-bundle 门,当前不可称为阶段实物 |
D-421 已关闭 D-420 记录的“Web Task mutation 必须独立设计”缺口,而且没有改名复用 run `33173769047` 的旧 archive。修复提交 `dc1686bd6fb3505174dd9a14098ae5c2c92a1a7f` 的普通主 CI [run 33229592307](https://github.com/whyour/qinglong/actions/runs/33229592307) 为 41 success/3 expected artifact-finalizer skip/0 fail,同源 Kubernetes deployment [run 33229592293](https://github.com/whyour/qinglong/actions/runs/33229592293) 成功;随后显式 Local Console milestone [run 33230227006](https://github.com/whyour/qinglong/actions/runs/33230227006) 为 42 success/2 scope skip/0 fail。由此 Web 创建能力已进入新的阶段实物,而不再只是候选源码。
+38 -2
View File
@@ -222,7 +222,7 @@ D-64 全部完成,也不得自动重启 2.x。
### 2.3 Docker adopted target 启动与重启屏障
ADR-0310 已补齐上述段落中的 Docker target barrierADR-0313 又补齐 Docker `manual_required` 的实例级
lineage、只读诊断与双阶段新 ceremony 授权。写后回退仍未完成。target 容器必须由 operator 预先创建但保持停止,并满足:完整 ID、不可变 digest image
lineage、只读诊断与双阶段新 ceremony 授权。写后回退仍未完成。target 容器必须由 operator 预先创建但保持停止,并满足:完整 ID、受审核镜像引用和精确 Docker content ID
`restart=no`、read-only rootfs、非 privileged、`no-new-privileges`,以及能把宿主机 Application v3 config、
legacy commitment、activation 和 source 精确映射到 config 中路径的唯一读写 bind mount。自动重启策略
`unless-stopped`/`always` 会绕过每代 Legacy recheck,因此 adopted target 不允许使用。
@@ -245,12 +245,19 @@ legacy commitment、activation 和 source 精确映射到 config 中路径的唯
"instanceId": "router-edge-1",
"activationPath": "/opt/qinglong/private/qinglong3-activation.json",
"legacySourcePath": "/opt/qinglong/data/database.sqlite",
"targetDatabasePath": "/opt/qinglong/data/database.ql3.sqlite",
"recoveryPath": "/opt/qinglong/data/database.recovery.sqlite",
"manifestPath": "/opt/qinglong/private/qinglong3-adoption.json",
"expectedLegacyDatabasePath": "/ql/data/database.sqlite",
"expectedActivationDigest": "REPLACE_WITH_64_HEX_ACTIVATION_DIGEST",
"expectedLegacyCommitmentDigest": "REPLACE_WITH_64_HEX_COMMITMENT_DIGEST",
"expectedLegacyContainerId": "REPLACE_WITH_FULL_64_HEX_LEGACY_ID",
"expectedTargetContainerId": "REPLACE_WITH_FULL_64_HEX_TARGET_ID",
"expectedTargetImage": "registry.example/qinglong3@sha256:REPLACE_WITH_64_HEX_DIGEST",
"targetImage": {
"authority": "registry-digest",
"reference": "registry.example/qinglong3@sha256:REPLACE_WITH_64_HEX_DIGEST",
"imageId": "sha256:REPLACE_WITH_DOCKER_IMAGE_CONTENT_ID"
},
"applicationConfigPath": "/opt/qinglong3/local-application.json",
"expectedTargetApplicationConfigPath": "/var/lib/qinglong3/local-application.json",
"expectedTargetCommitmentPath": "/var/lib/qinglong3/service/cutovers/router-edge-1-ql3/0002-legacy-stopped.json",
@@ -266,6 +273,12 @@ ql3-local-deploy cutover-target-start \
--command-file /secure/operator/qinglong3-target-start.json
```
正式 registry 部署必须使用 `authority=registry-digest``reference` 仍只接受不可变
`name@sha256:...``imageId` 从创建目标容器的同一 Docker daemon 读取。下载型 Alpha Trial Kit
没有可诚实声称的 registry RepoDigest,必须改用 `authority=local-image-id`、bundle manifest 中的
本地 image reference 和 exact image ID。两种模式都会同时核对 `docker inspect`
`Config.Image``.Image`,不会把 mutable local tag 单独当作权威。
controller 先写固定的 `0003-target-start-decision.json`,其中状态为
`target_start_requested`,然后至多调用一次 `docker container start <exact-id>`。只有同一容器处于
running、identity/mount/config digest 不漂移,且 Application 写出一个校验通过的新 Linux startup receipt
@@ -299,6 +312,29 @@ Legacy container 必须再次证明与 `0002` 相同的完整 identity/source bi
generation60 条 target journal record),达到上限后必须进入新的受审 cutover/recovery ceremony,不能清理
旧记录腾位置。
### 2.3.1 离线 Docker adopted target descriptor
`local.deployment.adopted.prepare|verify` 的 service 现在可以显式选择:
```json
{
"kind": "docker-target",
"targetImage": {
"authority": "local-image-id",
"reference": "qinglong3-local-application:ci-amd64",
"imageId": "sha256:REPLACE_WITH_TRIAL_KIT_IMAGE_ID"
},
"allowRootService": false
}
```
它生成 `service/docker-target.json` 和同一份 Application v4 配置,固定 numeric UID:GID、
`restart=no`、无网络、read-only rootfs、drop ALL、no-new-privileges、Profile memory/PID 上限、
deployment root 可写 mount 与 legacy source 只读 mount。该 descriptor 是内容绑定的创建输入,
不是启动授权;operator 仍须先按 descriptor 创建并保持容器停止,再把容器完整 ID 和同一个
`targetImage` 交给 `target-start`。因此离线 Trial Kit 不需要伪造 GHCR catalog/release selection
正式 Compose 路径也不因 Alpha 便利性而放宽。
### 2.4 `manual_required` 诊断与双阶段新 Ceremony
每个实例的当前 cutover 由以下私有 CAS head 固定: