跳转到主要内容

Managed 工作区

工作区 → 仓库 → Dist 三层模型、固定磁盘布局、sow.yml 如何驱动一切,以及发现与选择规则。

当同一个仓库要维护好几个月 —— 包成批到达、由策略决定谁留下、事后还得说清楚什么时候变了什么 —— 你需要的是 Managed 模式。本页讲三层模型、它产出的布局,以及命令怎么判断你说的是哪个仓库、哪个 Dist。

三个层级

Workspace 工作区                    发现与配置边界
└── Repository 仓库                 所有权边界:pool、dists、SQLite、锁、Generation
    └── Dist 发行版                 单一格式的具名成员集合
        └── Architecture View 架构视图   渲染投影 —— 不是成员关系

每层只做一件事,边界很硬:

工作区(Workspace) 只拥有两样东西:根级 sow.yml.sow/ 状态目录。工作区根下其他任何东西都不属于 SOW。它是发现的单位 —— 命令从某个起始目录向上找到工作区 —— 也是架构许可表所在的地方。

仓库(Repository) 固定在 <workspace>/<name>。你不能把它指到别处,没有 path 选项。一个 Repository 拥有自己的 pool/dists/、SQLite、锁、恢复状态、Generation、保留代根、发布 checkpoint 与 GC 证据。两个 Repository 之间永不去重 —— 同一个包 add 进两个仓库就存两份,这是刻意的:这样删掉一个仓库永远不可能伤到另一个。

Dist 是一个单一格式(rpmdeb)的普通具名成员集合。名字对 SOW 是不透明字符串。el9trixieel9-betacustomer-acme —— 它们都不产生状态机、晋升流程或快照。想要一个 beta 频道,就建一个叫 el9-beta 的 Dist;含义存在于你的脑子和 .repo 文件里,不在 SOW 里。

架构视图(Architecture View)build 渲染出来的东西,不产生第二份成员关系。一个 noarch RPM 只有一个包对象、一条成员记录,构建时投影进每个适用视图。参见包池与元数据视图

一个 Repository 可以同时拥有 RPM Dist 与 DEB Dist,共用同一个 pool/

磁盘布局

Managed 路径从不由用户输入拼装,而是由解析后的真实工作区根、已校验的名称和固定相对片段推导出来 —— 这就是为什么符号链接替换和路径逃逸没有可攻击的面。

<workspace>/
├── sow.yml                       # 唯一的配置文件
├── .sow/                         # 私有状态;绝不要对外服务
│   ├── workspace.lock
│   ├── workspace-ops/            # 工作区生命周期 journal
│   ├── repo-locks/<repo>.lock
│   ├── <repo>.db                 # 每个仓库一个 SQLite
│   └── <repo>/
│       ├── stage/                # 同文件系统的 staging 区
│       ├── recovery/             # 删除动作的原子移入目标
│       └── pending/              # --skip 的耐久 payload
└── <repo>/                       # 可对外服务的树
    ├── pool/                     # 不可变包字节
    └── dists/
        └── <dist>/               # 架构视图渲染在这里

执行过两次 dist new 与两次 add 之后的真实工作区:

$ find .sow | sort
.sow
.sow/pigsty
.sow/pigsty.db
.sow/pigsty.db-shm
.sow/pigsty.db-wal
.sow/pigsty/pending
.sow/pigsty/recovery
.sow/pigsty/stage
.sow/repo-locks
.sow/repo-locks/pigsty.lock
.sow/workspace-ops
.sow/workspace.lock

<repo>/ 下是公共交付树:可以直接服务、由 SOW 发布,或整根复制到离线 staging 后原子切换。 .sow/ 下全部是私有状态,绝不能暴露;详见对外服务

名称必须匹配 [a-z0-9][a-z0-9._-]*....sowpooldists 以及工作区保留名一律拒绝。

状态数据库与软件包事实

私有 SQLite 数据库按 package_sha256 索引 Desired/Built Membership,因此查询与构建可以 一次批量展开完整成员投影,而不是为每个软件包单独查询。数据库还保存以不可变包体 SHA-256 为键、可重建的软件包事实缓存。Ingest 只需完整认证并解析每个新软件包一次;生产构建只按本次 选中的 Digest,以有界、确定性批次读取 Facts Row,遇到缺失或损坏记录时再从已认证包体惰性 重建。无关或超大的 Facts Row 不会被读取。

对于未改变的 Pool 文件,暖构建使用设备号、inode、size、mtime 与 ctime 指纹避免重新读取 包体。指纹漂移与 Facts 缺失共享一次权威 SHA-256 校验并自动修复; sow check 仍是显式的完整密码学审计,并且无论指纹是否匹配,都会 对每个唯一物理包体哈希一次。

缓存与指纹只属于私有实现状态,不改变公共 pool/ + dists/ 布局。不要手工编辑数据库或 PRAGMA user_version。v0.3 Repository 必须先备份,再通过 sow repo migrate 显式升级,之后才能执行普通 0.4 读写。全新 0.4 Repository 已使用当前 Schema;其他维护只在 SOW 诊断明确指出时执行。

sow.yml 驱动一切

只有一个配置文件,用严格 decoder 解析。未知字段不会被忽略——它会失败。重复的规范化架构、非法名称或 format、Dist 架构不是工作区许可表的子集、非法 glob 或分类、不完整的 signing 块,同样失败。

schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
      trixie:
        format: deb
targets:
  local:
    repository: pigsty
    provider: filesystem
    endpoint: file:///srv/mirror
    prefix: pigsty
    public_endpoint: file:///srv/mirror/pigsty/
    max_cache_ttl: 0s
    authoritative_workspace: true
    single_writer: true
    exclusive_write_authority: true

config show --all 展开全部默认值与规范化别名,让你看到 SOW 实际的决定:

$ sow config show --all
schema: sow/v3
architectures:
  - x86_64
  - aarch64
repos:
  pigsty:
    protected: false
    signing:
      rpm:
        packages:
          mode: never
    dists:
      el9:
        format: rpm
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []
      trixie:
        format: deb
        architectures:
          - x86_64
          - aarch64
        limit: 0
        exclude: []

架构别名只在解析边界规范化一次:amd64 → x86_64,arm64 → aarch64。输出永远是 canonical family。生态名只在渲染出来的 DEB 视图目录名里出现(binary-amd64binary-arm64)。

config check 不是 YAML lint。它会打开每个已初始化 Repository 的 SQLite,把候选配置与实际的 Dist、架构、成员集、Built 状态和签名可用性逐项比对。移除仍被成员或 Built 状态引用的架构族是预期拒绝(退出码 6);数据库或协议证据损坏是完整性错误(退出码 5)。每个写命令在写 journal 之前都跑同一套预检,所以 config check 能提前告诉你下一条 add 会不会被拒。

$ sow config check
configuration valid: /data/ws repositories=1 dists=2

完整 schema(包括 filesystemr2 发布目标)见 sow.yml 配置参考

发现:哪个工作区?

Managed 命令按以下顺序寻找最近祖先中的 sow.yml:

  1. 给了 -C/--workdir DIR:从 DIR 向上找,并跳过当前目录候选。
  2. 否则从当前目录向上找。
  3. 仍未找到:从 $SOW_DIR 向上找;显式 -C 查找失败后也保留这条回退。
  4. 还是没有:失败,并提示 sow init--workdirSOW_DIR

找到第一个 sow.yml 就停,不会越过它继续往上找"更好的那个"。

--workdir 不是 chdir。它只改变发现的起点。sow add 里的相对 PATH 仍然相对你真实的当前目录解析 —— 这正是 sow add ./build/*.rpm -C /srv/ws 应有的行为。

sow create 不参与上述任何一步。

选择:哪个仓库、哪个 Dist?

Repository 选择,按序:

  1. 显式 -r/--repo NAME
  2. 命令起始目录位于 <workspace>/<repo>/ 内。
  3. 工作区只有一个 Repository。
  4. 否则失败并列出候选。

Dist 选择,按序:

  1. 一个或多个显式 -d/--dist NAME(可重复)。
  2. 起始目录位于 <workspace>/<repo>/dists/<dist>/ 内。
  3. 选定 Repository 只有一个 Dist。
  4. 否则失败并列出候选。

关键的不对称在这里:没给 -d 时,buildcheckstatus 默认作用于选定 Repository 的 全部 Dist —— 对这几个命令而言,“没有过滤条件"解释成"全都要"是安全的。而 addrmls 必须得到明确的 Dist 集合,因为猜一个包该落到哪里并不安全:

$ sow ls
workspace discovery error: repository "pigsty" has multiple Dists (el9, trixie); select one or more with --dist

退出码 2。所有推断都在路径类型与符号链接校验之后才进行。

init 只做收敛,不做重置

sow init 的幂等性是设计出来的,它的规则是架构不变式而不是使用便利:

  • 没有 sow.yml: 创建一个,写入 schema: sow/v3architectures: [x86_64, aarch64],同时创建 .sow/。不自动创建任何 Repository。
  • 已有合法配置: 按稳定名称顺序补齐尚未初始化的部分 —— 缺失的 Repository 外壳、缺失的 SQLite、整个缺失的 Dist。新建的 Dist 立刻生成其当前有效架构的全部空视图。
  • 已有有效数据库状态或有效协议指针: 只校验。绝不覆盖、绝不清零 Generation、绝不重写字节。
  • Dist 已初始化之后又往配置里加了架构: init 不渲染新视图、不推进 Generation。该 Dist 保持 dirty,等待显式 build。移除仍被成员或 Built 状态使用的架构族则失败。
$ sow init .
initialized /data/ws: config_created=false repositories_initialized=0 dists_initialized=0

第三、第四条规则的意义在于:init 必须能安全地在装着真实内容的仓库上执行。它朝声明的配置收敛,但绝不会拿"还没初始化"当借口去重建一个本来就好好的东西。

对象按稳定顺序处理。如果靠前的配置、Repository 或 Dist 已经耐久提交,而靠后的对象失败了,已提交的计数会被保留:人类输出先报告已提交的结果,--json 保留结构化 result,命令以 3(部分成功)退出。如果此时还没有任何东西提交过,则按原始错误类别退出。

空 Dist 也有合法协议发布面

dist new 建出来的 Dist,在 add 第一个包之前就已具备完整协议入口。RPM Dist 每个架构族 有一份合法的空 repodata;DEB Dist 有 PackagesPackages.gz、by-hash 条目和 Release。如果 Repository 配了元数据密钥,空 Dist 也一样签名。

因此从 Dist 里移除最后一个包之后,留下的是一份合法的空索引(配置签名时仍签名), 而不是缺失或损坏的协议入口。真实包管理器验收仍是另一层兼容性门禁。

Protected 仓库

repos:
  pigsty:
    protected: true

protected: true 会拒绝 repo rm,即使加了 -f,返回退出码 6。它不限制别的:addrmbuild 和常规 Dist 维护照常。真要删这个 Repository,你必须先改 sow.yml、通过 config check,然后才能删 —— 这个摩擦正是该 flag 存在的意义。

继续阅读