Files
qinglong/docs/adr/ADR-0197-generation-cas-local-compose-image-selection.md

6.0 KiB
Raw Permalink Blame History

ADR-0197:基于 Generation CAS 的本机 Compose 镜像选择

  • 状态:Accepted(选择事务与产品 CLI 已实现;真实切流和设备门仍待完成)
  • 日期:2026-07-29
  • 关联 RFCQL-RFC-0001 D-184、D-185、D-186、D-187
  • 关联 ADRADR-0194、ADR-0195、ADR-0196

背景

D-184 生成包含不可变镜像 digest 的 compose.yaml,适合首次部署,却把长期不应 变化的 sandbox、资源限制、volume 和网络策略与每次发布都变化的镜像选择放在同一 文件。直接允许覆盖该文件会同时扩大升级 authority,并且在进程中断、两个 operator 竞争或回退多代版本时无法区分安全模板漂移和合法镜像变化。

控制器若直接调用 docker compose pull/up/down,又会把本地文件提交、远端 registry、 Docker daemon、进程健康和 SQLite capability 合并成一个实际上不具备 ACID 的操作。 在路由设备上,这还会扩大短生命周期 CLI 的依赖和常驻成本。

决策

1. 稳定模板与可变选择分离

Compose 基础 descriptor 固定不携带 image;私有 selection 是第二个 override 文件:

x-qinglong-image-selection:
  schema: qinglong/local-compose-image-selection@v1
  generation: 1
services:
  qinglong3:
    image: registry.example/qinglong3-local@sha256:...

operator 必须按固定顺序同时传入 service/compose.yaml 和私有 service/compose.image.yaml

docker compose \
  -f /opt/qinglong3/service/compose.yaml \
  -f /opt/qinglong3/service/compose.image.yaml ...

compose.image.yaml 是唯一活动镜像选择,权限为当前 UID 0600。它同时记录 schema、 generation、previous generation、rollback target、mutation、变更时间和完整 OCI digest。字段采用固定顺序和安全 YAML 子集;未知、重复、非 canonical 或 mutable image 均拒绝。它不使用 Compose interpolation,因此 shell、.env 或 ambient environment 不能覆盖 image authority。

ADR-0198 在不改变 selection authority 的前提下,为 service 重复派生 generation/mutation label,并为基础模板加入 instance-scoped project nameparser 要求 metadata 与 label exact 相等。

D-184 fresh prepare 为 Compose 创建 generation 1,并在 service/revisions/1.yaml 保存完全相同的不可变记录。revision 目录为当前 UID 0700

2. Upgrade 和 rollback 都创建新 generation

入口继续属于既有 @qinglong/local-owner-cli/local-deployment

ql3-local-deploy compose-revision \
  --command-file /absolute/private-command.json

upgrade command 固定当前 expectedGeneration、新的 @sha256 image、 UUID v4 mutation 和时间。rollback 不接受调用方重新提交旧 image,只接受小于当前 generation 的 targetGeneration;控制器从同一 revision 目录读取、复核并复制其 image。回退也创建新的 generation,而不是把 head 数字倒退,因此历史和 CAS 始终 单调。

每次操作最多读取当前 selection、一个 rollback target 和本次 revision,使用常数级 内存。generation 上限为 100,000;每代只增加一个不足 64 KiB、实际为数百字节的 私有文件,不引入数据库表、timer、watcher、listener、第三方依赖或第 23 个 workspace package。

3. 单文件原子切换和结果未知恢复

控制器按以下顺序执行:

  1. 复核 deployment、service、revision 目录的 canonical/current-UID/0700 identity
  2. 严格读取 active selection,要求当前 generation 等于 expected,或已经是同一 mutation 的 exact next generation
  3. 以完整 normalized command 创建 deterministic no-replace lock;不同 intent 冲突失败关闭;
  4. 再读 active selection,关闭 lock 获取窗口中的变更;
  5. no-replace 发布新的 immutable revision
  6. 在同一目录完整写入 stage、fsync,再次复核 expected active bytes,再以 rename 原子替换 compose.image.yamlfsync service 目录;
  7. 精确复核并移除 command lock。

崩溃发生在 stage、revision publication、active rename 或 lock cleanup 窗口时, operator 必须原样重放同一 command。若 active 已是 exact next selection,重放只补齐 缺失 revision/清理 lock 并返回 existing;不得换 mutation 或时间来猜测结果。 stale generation、active/revision/stage/lock 的 identity 或内容漂移均拒绝。

4. Selection activation 不等于运行中容器切流

本控制器明确不:

  • 拉取、构建或验证镜像;
  • 调用 Docker daemon 或 Compose
  • start、stop、restart 服务;
  • 打开、迁移、备份或降级 SQLite
  • 删除历史 revision
  • 宣称旧 image 一定兼容当前数据库 capability。

operator 必须先按 D-186 验证 exact release digest 并预取镜像,再提交 selection, 检查 docker compose ... config --images,最后显式执行 up -d 并观察应用 active/stopped 事件。镜像选择回退只恢复 desired image,不等于数据回退;目标 image 与当前 SQLite capability 的兼容证据属于独立 release gate。

验收

  • Compose fresh prepare 生成稳定模板、generation 1 active selection 和 immutable revision
  • upgrade 1 → 2、exact replay 和 rollback 2 → 3(target=1) 收敛;
  • rollback 只读取历史 revision,历史 drift 拒绝;
  • stale expected generation 和不同 command lock 拒绝;
  • stage 与 active-switch response-loss 窗口可由原命令恢复;
  • CLI 只接收私有 command file,输出仅含 operation/status/generation/kind
  • 本机 Docker Compose v5.3.1 双 -f config --images 精确返回 selection 中的 digest
  • local-owner-cli 全量测试通过,workspace package 和生产依赖数量不变。

未包含

  • 自动 Docker service activation 和健康回退;
  • systemd/OpenRC 二进制或系统包 revision
  • SQLite schema/capability downgrade
  • remote signature verification 与真实 GHCR release
  • 路由器断电、ENOSPC、闪存寿命和镜像垃圾回收策略。