跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

教程

端到端实操:从一组软件包文件开始,构建客户端可直接使用的已签名软件仓库。

这里的教程都从全新工作区开始。请按顺序执行命令,并按你的环境替换大写占位符与包路径。

如果还没装 SOW,先看安装快速上手。 下面的教程介绍 Managed 仓库路径。

托管 RPM 仓库:分架构视图、noarch 中性投影、debuginfo 过滤、版本数量上限,以及可用的 dnf 客户端配置。

托管 DEB 仓库:Debian 风格包池、by-hash 索引与 deb822 客户端配置。

生成专用 GPG 签名钥,为仓库元数据与 RPM 包签名,并配置客户端拒绝一切未签名内容。

用 Nginx 服务 Repository,并把已校验 Generation 发布到配置好的 filesystem target, 同时避免暴露工作区私有状态。

把 infra-pkg 已产出的双架构 RPM 与 DEB 组织成真实仓库,并演练本地安装、滚动更新、 Stable 晋升与月度快照。

先看哪篇

你的处境 从这里开始
你要向 dnf 客户端分发 RPM 搭建 YUM 仓库
你要为 Debian 或 Ubuntu 分发 DEB 搭建 APT 仓库
需要已签名元数据或已签名 RPM 包体 仓库签名
树已经建好,但外部无法访问 对外服务
想把现有双架构包池变成可维护的 Infra 仓库 演练构建 pigsty-infra 仓库

YUM 与 APT 两篇是彼此独立的全新 Workspace 路径。实际使用中,如果它们适合共用同一所有权 边界,一个 Workspace 的同一 Repository 可以同时容纳 RPM 与 DEB Dist。

本板块约定

Shell 代码块里的命令不带 $ 提示符,方便整块复制。输出单独成块放在命令下方,只有一行时用注释 标注。需要你自行替换的值一律写成 大写

每篇教程结尾都有验证步骤。sow check 返回 0,才证明所选 Repository 完整且与记录的 Generation 一致;非零结果不应作为交付物。

1 - 搭建 YUM 仓库

创建托管 RPM 仓库,配置成员策略,对外服务并接入 dnf。

本教程从零创建一个 Managed RPM 仓库。你需要一个可写目录,以及一个或多个 RPM 文件。

1. 创建工作区

mkdir -p /srv/sow
cd /srv/sow
sow init .
sow repo new pigsty
sow dist new el9 --format rpm -r pigsty

Dist 名称由你定义。SOW 不会根据 el9 推断操作系统版本。

2. 配置成员策略

编辑生成的 sow.yml。下面的配置按包名与架构各保留一个版本,并排除调试包:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  pigsty:
    dists:
      el9:
        format: rpm
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource, llvmjit]
targets: {}

手工修改配置后,先校验再写仓库状态:

sow config check
sow config show --all

策略先执行 exclude,再执行 limitnoarch 包会投影到每个启用的架构视图,不能写进 architectures

3. 添加 RPM

sow add /path/to/packages/*.rpm -r pigsty -d el9
sow status -r pigsty
sow check -r pigsty

add 解析包头,将每个接纳的包只保存一次,更新 Desired 成员关系并落成新 Generation。 被策略排除的输入会逐项报告,但不算命令失败。

公共树如下:

/srv/sow/pigsty/
├── pool/...
└── dists/el9/
    ├── x86_64/repodata/...
    └── aarch64/repodata/...

rpm-md 的 location href 通过相对路径访问根目录下的 pool/。不要只复制某个架构目录; 它不是独立仓库。

4. HTTP 预览

本地预览可以直接使用:

cd /srv/sow
python3 -m http.server --bind 127.0.0.1 8080

在另一个终端检查入口:

curl --fail http://127.0.0.1:8080/pigsty/dists/el9/x86_64/repodata/repomd.xml >/dev/null

长期服务应使用持续维护的 HTTP Server。它必须完整暴露 pigsty/ 树,确保客户端解析后落到 pigsty/pool/ 的软件包 URL 可访问。

5. 配置 dnf

REPO_HOST 替换为客户端可访问的地址:

# /etc/yum.repos.d/pigsty.repo
[pigsty-el9]
name=Pigsty EL9
baseurl=http://REPO_HOST:8080/pigsty/dists/el9/$basearch/
enabled=1
gpgcheck=0
repo_gpgcheck=0

刷新并查询仓库:

sudo dnf clean metadata
sudo dnf makecache --refresh
dnf --disablerepo='*' --enablerepo=pigsty-el9 list available

这里有意使用未签名配置。只有完成仓库签名后,才应打开客户端验签。

6. 发布或导出

交付前必须通过深度校验:

sow check -r pigsty

向已配置的 filesystem 或 R2 目标交付时,使用 sow publish。 只有先复制到离线 staging、再原子切换上线时,才适合整根复制;不要对在线仓库做无序原地同步。

默认 dnf reposync 等工具会拒绝指向根包池的父级相对路径。遇到这种消费者时,导出一份 自包含 RPM Leaf:

sow export rpm-leaf el9 x86_64 /srv/export/pigsty-el9-x86_64 -r pigsty

目标目录必须不存在或为空。默认会复制包体;--hardlink 只适用于同一文件系统、可信且只读的 显式优化场景。

更新仓库

sow add /path/to/new.rpm -r pigsty -d el9
sow rm PACKAGE_NAME -r pigsty -d el9
sow build -r pigsty
sow check -r pigsty

addrm 修改 Desired 成员关系;策略或签名配置变化后用 build 收敛;发布门禁是 check,不能只看 status

自动化客户端与平台覆盖见平台与集成

2 - 搭建 APT 仓库

创建带 by-hash 索引的托管 DEB 仓库,并配置 APT 客户端。

本教程从零创建一个 Managed DEB 仓库。你需要一个可写目录,以及一个或多个 DEB 文件。

1. 创建工作区

mkdir -p /srv/sow
cd /srv/sow
sow init .
sow repo new pigsty
sow dist new trixie --format deb -r pigsty

Dist 名称会成为 APT Suite。它由你定义;SOW 不会根据 trixie 推断发行版语义。

2. 配置成员策略

如果需要过滤或限制版本,编辑生成的 sow.yml

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  pigsty:
    dists:
      trixie:
        format: deb
        limit: 1
        exclude:
          - kind: [dbgsym, dbg]
targets: {}

然后校验:

sow config check
sow config show --all

配置中保存规范架构名,渲染时使用 Debian 生态名称:x86_64 对应 amd64aarch64 对应 arm64;中立架构 all 包会进入两个视图。

3. 添加 DEB

sow add /path/to/packages/*.deb -r pigsty -d trixie
sow status -r pigsty
sow check -r pigsty

接纳的包体只保存一次。公共树如下:

/srv/sow/pigsty/
├── pool/...
└── dists/trixie/
    ├── Release
    └── main/
        ├── binary-amd64/
        │   ├── Packages
        │   ├── Packages.gz
        │   └── by-hash/SHA256/...
        └── binary-arm64/...

pool/ 下的路径按规范化源码包名分组;Packages 中的 Filename 相对 Archive Root; SOW 会写入 SHA-256 by-hash 副本,并在 Release 中声明。

4. HTTP 预览

本地预览可以直接使用:

cd /srv/sow
python3 -m http.server --bind 127.0.0.1 8080

检查协议入口:

curl --fail http://127.0.0.1:8080/pigsty/dists/trixie/Release >/dev/null
curl --fail http://127.0.0.1:8080/pigsty/dists/trixie/main/binary-amd64/Packages.gz >/dev/null

长期服务应使用持续维护的 HTTP Server,并完整暴露 pigsty/ 树。

5. 配置 APT

REPO_HOST 替换为客户端可访问的地址。未签名测试仓库可使用显式信任的 deb822 配置:

# /etc/apt/sources.list.d/pigsty.sources
Types: deb
URIs: http://REPO_HOST:8080/pigsty
Suites: trixie
Components: main
Architectures: amd64
Trusted: yes

刷新并查询:

sudo apt update
apt-cache policy

Trusted: yes 会关闭真实性校验,只适合受控测试。签名仓库应删除该行并配置 Keyring:

Types: deb
URIs: https://repo.example.com/pigsty
Suites: trixie
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/pigsty-archive-keyring.gpg

打开 Signed-By 前,请先完成仓库签名

6. 安全发布

交付前必须通过深度校验:

sow check -r pigsty

向已配置的 filesystem 或 R2 目标交付时,使用 sow publish。 如果使用其他传输方式,应把完整仓库复制到离线 staging,再原子切换上线。不要逐文件更新在线 dists/ 树,否则客户端可能同时看到不同 Generation 的元数据与包体。

更新仓库

sow add /path/to/new.deb -r pigsty -d trixie
sow rm PACKAGE_NAME -r pigsty -d trixie
sow build -r pigsty
sow check -r pigsty

策略或签名配置变化后用 build 收敛;发布门禁是 check,不能只看 status

自动化客户端与平台覆盖见平台与集成

3 - 仓库签名

签署 RPM 与 APT 元数据,可选签署 RPM 包体,并启用客户端验签。

SOW 提供两条互相独立的签名路径:

路径 产物 客户端开关
RPM 元数据 repodata/repomd.xml.asc repo_gpgcheck=1
APT 元数据 InReleaseRelease.gpg Signed-By
RPM 包体 RPM 内嵌签名 gpgcheck=1

APT 通过签名的 Release 信任包哈希;SOW 不重签 DEB 包体。先配置元数据签名;只有当你负责 这些 RPM 字节的签名策略时,再启用包体签名。

1. 创建专用密钥

下面生成一把无口令的示例密钥。生产环境应使用受保护密钥并配置 passphrase 引用,详见 配置参考

SIGNING_UID='SOW Repository <[email protected]>'
gpg --batch --pinentry-mode loopback --passphrase '' \
  --quick-generate-key "$SIGNING_UID" rsa3072 sign 2y

FPR="$(gpg --batch --with-colons --list-secret-keys "$SIGNING_UID" \
  | awk -F: '$1 == "fpr" {print $10; exit}')"
test -n "$FPR"

sudo install -d -m 0700 /srv/sow-secrets
sudo chown "$(id -u):$(id -g)" /srv/sow-secrets
gpg --batch --pinentry-mode loopback --passphrase '' --armor \
  --export-secret-keys "$FPR" > /srv/sow-secrets/repo-signing.asc
gpg --armor --export "$FPR" > /srv/sow-secrets/repo-signing.pub
chmod 600 /srv/sow-secrets/repo-signing.asc

私钥必须放在 Workspace 公共 Repository 树与所有 Web Root 之外。若 SOW 由专用服务账户运行, 目录 owner 应是该账户,而不是交互用户。只向客户端分发 repo-signing.pub

2. 配置元数据签名

/srv/sow/sow.yml 的 Repository 下添加所需配置;未使用的包生态可以省略:

repos:
  pigsty:
    signing:
      rpm:
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc
      deb:
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc
    dists:
      # 保留已有 Dist 定义

解析密钥引用、重建并执行发布门禁:

cd /srv/sow
sow config check
sow build -r pigsty
sow check -r pigsty

受口令保护的密钥可在 key 旁增加 passphrase: env://SOW_METADATA_PASSPHRASE,或使用 有界文件引用。SOW 不会把密钥或口令内容写入配置、SQLite、JSON 或日志。

3. 手工验证元数据

按实际 Dist 与架构调整路径:

gpg --verify \
  pigsty/dists/el9/x86_64/repodata/repomd.xml.asc \
  pigsty/dists/el9/x86_64/repodata/repomd.xml

gpg --verify pigsty/dists/trixie/InRelease
gpg --verify \
  pigsty/dists/trixie/Release.gpg \
  pigsty/dists/trixie/Release

sow check 会在深度一致性校验中检查配置的签名身份。建立客户端信任根时,仍应手工验证一次。

4. 可选:签署 RPM 包体

只有客户端要求内嵌包签名时,才添加 rpm.packages

repos:
  pigsty:
    signing:
      rpm:
        packages:
          mode: fill
          key: agent://REPLACE_WITH_THE_FINGERPRINT
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc

将占位符替换为 $FPR 中的 40 位十六进制指纹。该操作要求:

  • 安装 rpmgpg
  • 匹配的私钥存在于 rpm 使用的环境 GPG Keyring 中;
  • fill 保留已经由配置 key 或 trusted_keys 签好的包;
  • always 重签所有未由配置身份签好的包;
  • never 保持输入字节不变。

SOW 只会对私有 staged 副本调用 rpm --addsignrpm --resign,不会修改输入文件。 修改策略后重新校验并构建:

sow config check
sow build -r pigsty
sow check -r pigsty

rpmkeys --checksig /path/to/package.rpm 检查结果。

5. 启用 dnf 验签

通过可信通道把公钥传到客户端:

sudo install -m 0644 /path/to/repo-signing.pub /etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty

再打开与你实际签名范围对应的检查:

[pigsty-el9]
name=Pigsty EL9
baseurl=https://repo.example.com/pigsty/dists/el9/$basearch/
enabled=1
repo_gpgcheck=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty

没有配置包体签名时,将 gpgcheck 设为 0;既然已经配置元数据签名,就不要关闭 repo_gpgcheck

6. 启用 APT 验签

将公钥安装为独立 Keyring:

sudo gpg --dearmor --yes \
  --output /usr/share/keyrings/pigsty-archive-keyring.gpg /path/to/repo-signing.pub

在 deb822 配置中引用它,且不要设置 Trusted: yes

Types: deb
URIs: https://repo.example.com/pigsty
Suites: trixie
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/pigsty-archive-keyring.gpg

执行 apt update。任何签名错误都应视为部署失败,不能靠削弱客户端配置绕过。

Plain 模式 RPM 签名

Plain 模式可以签署 RPM 包体,但不会签署仓库元数据,也不会生成 APT Release

sow create /srv/flat --sign-with 0123456789ABCDEF

key 必须是恰好 16、40 或 64 位十六进制字符,不能带 0x 前缀;匹配私钥必须能被环境中的 rpm/GPG 使用。不带 --overwrite 时,已有签名的 RPM 保持字节不变;带上该参数则显式重签 所有 RPM。SOW 先签署私有 staged 副本,再替换包体与元数据。

更换密钥

改变 key 引用或解析出的指纹会让相关 Dist 变为 dirty。元数据 key 可以先分发新公钥,再重建、 校验并切换客户端。RPM 包 key 必须分阶段轮换:Package Object 不可变,build 遇到不满足新策略的 既有 RPM 会拒绝,而不是原地重签。旧软件包坐标尚未下架或由新 Release 替代前,应使用 fill 并把旧公钥保留在 trusted_keys。最后在目标环境做真实客户端验收。

最后应使用生产中的确切 dnf/APT 版本与信任策略验收签名仓库。自动化覆盖见 平台与集成

4 - 服务与发布仓库

用 Nginx 服务公共 Repository,并把已校验 Generation 发布到 filesystem target。

SOW 只生成静态文件,不是 HTTP 服务器。本指南把可写 Workspace 与 Nginx 服务路径分开。

公共与私有路径

模式 公共单元 绝不能服务
Plain 传给 sow create 的目录 临时 .sow-plain-stage-* 输出;没有持久 journal
Managed 一个 Repository 的完整 pool/ + dists/ Workspace sow.yml.sow/、SQLite、锁、日志与 staging

第一个工作区里的源 Repository 是 /srv/sow/local。不要把 /srv/sow 设为 document root。

1. 校验源 Generation

cd /srv/sow
sow build -r local
sow check -r local

只有 check 返回 0 才继续。status 适合诊断;check 才是完整只读交付证明。

2. 配置 filesystem target

先创建 endpoint 目录。它必须是真实、规范目录,不能是 symlink;SOW 不会替你创建缺失 endpoint。

sudo install -d -m 0755 /srv/repo-public
sudo chown "$(id -u):$(id -g)" /srv/repo-public

第二条命令把写权限交给当前操作者;若 sow publish 由专用服务账户执行,应改为该账户。

/srv/sow/sow.yml 中增加 target:

targets:
  public:
    repository: local
    provider: filesystem
    endpoint: file:///srv/repo-public
    prefix: local
    public_endpoint: file:///srv/repo-public/local/
    max_cache_ttl: 0s
    authoritative_workspace: true
    single_writer: true
    exclusive_write_authority: true

三个布尔字段都是必填安全确认。endpoint 与 prefix 合并为 /srv/repo-public/local; SOW 会在预先存在的 endpoint 下创建并拥有该 prefix。

校验并发布:

sow config check
sow publish public

发布先复制不可变包体和元数据,再更新可变协议指针,随后校验结果并记录 target checkpoint。 同一 Generation 重复发布是幂等空操作。

不要让其他工具写入同一 target prefix。target 契约是单 writer、独占写入。

3. 用 Nginx 服务 target

server {
    listen 80;
    server_name repo.example.com;

    root /srv/repo-public;
    autoindex off;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ (^|/)\. {
        deny all;
    }
}

校验配置后 reload Nginx。客户端 URL 为:

DNF baseurl: http://repo.example.com/local/dists/el9/x86_64/
APT source:  deb http://repo.example.com/local bookworm main

如果元数据或包体已签名,请单独发布对应公钥并配置 gpgkey/Signed-By;私钥绝不能放在 document root 下。

4. 验证服务入口

curl --fail --head \
  http://repo.example.com/local/dists/el9/x86_64/repodata/repomd.xml

curl --fail --head \
  http://repo.example.com/local/dists/bookworm/Release

curl --fail --head \
  http://repo.example.com/local/dists/bookworm/main/binary-amd64/Packages.gz

随后从客户端主机运行真实包管理器。HTTP 可达不等于客户端已验证,两层都要检查。

完整 Repository prefix 必须使用同一访问策略。RPM 元数据可能通过 ../../../pool/... 解析包路径,APT Filename 也直接指向 pool/...。只保护 dists/ 而误放开或拦截 pool/ 都会破坏仓库。

手工与隔离交付

如果 sow publish 无法到达目标:

  1. 在源端运行 sow check
  2. 把完整 Repository 复制到新的、非 live staging/release 目录;
  3. sow changes 0 或 archive manifest 校验传输哈希;
  4. 原子切换操作者拥有的父级引用到新目录;
  5. 保留上一版,直到客户端与缓存越过它。

不要直接对 live Repository root 执行无序 rsync --delete。它不保留 SOW 的指针顺序、 target checkpoint、缓存 grace 或恢复状态。sow changes 描述 Generation 差异,不代表可以 绕过这些控制直接修改 live target。

R2 target

provider: r2 使用 S3 兼容存储传输与只报告的 target GC。传输集成会针对固定 MinIO fixture 验证 list、HEAD、GET 与条件 PUT。启用生产目标前,请先在非生产 prefix 验证凭据、bucket policy、公共 endpoint、缓存行为、重放与恢复。详见平台与集成

下一步

5 - 演练构建 pigsty-infra 仓库

把既有的双架构 RPM 与 DEB 包池组织成 infra 仓库,完成本地安装验收、滚动更新、Stable 晋升与月度快照。

pgsty/infra-pkg 是 Pigsty Infra 软件包的上游构建源码。 本教程假设双架构 RPM 与 DEB 已经构建完成,只处理后半程:从一堆包开始,用 SOW 建成真正可消费、可维护的 infra 仓库。

1. 把包集中到 ~/repo

本教程固定使用 ~/repo,不再为每条路径定义环境变量。先把已有包复制进两个输入目录:

mkdir -p ~/repo/packages/rpm ~/repo/packages/deb
cp ~/pgsty/infra-pkg/dist/rpm/*.rpm ~/repo/packages/rpm/
cp ~/pgsty/infra-pkg/dist/deb/*.deb ~/repo/packages/deb/

先确认四个“格式 × 架构”象限都有真实包体:

find ~/repo/packages/rpm -maxdepth 1 -type f -name '*.x86_64.rpm' | wc -l
find ~/repo/packages/rpm -maxdepth 1 -type f -name '*.aarch64.rpm' | wc -l
find ~/repo/packages/deb -maxdepth 1 -type f -name '*_amd64.deb' | wc -l
find ~/repo/packages/deb -maxdepth 1 -type f -name '*_arm64.deb' | wc -l

四个结果都必须大于零。此时目录只有输入包池:

~/repo/
└── packages/
    ├── rpm/                         # x86_64 + aarch64 RPM
    └── deb/                         # amd64 + arm64 DEB

2. 创建 infra Repository 与两个 Dist

初始化 Workspace,并创建名为 infra 的 Repository:

sow init ~/repo
cd ~/repo
sow repo new infra
sow dist new rpm --format rpm -r infra
sow dist new deb --format deb -r infra

现在模型已经确定:

Repository: infra
├── Dist: rpm    format=rpm    policy=latest
└── Dist: deb    format=deb    policy=latest

打开 ~/repo/sow.yml,把配置整理为:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  infra:
    dists:
      rpm:
        format: rpm
        limit: 1
      deb:
        format: deb
        limit: 1

limit: 1 按“包名 + 原生架构”只保留最新一个版本。因此 rpmdeb 就是两个滚动更新的 latest channel;它们仍会同时保留 x86-64 与 ARM64 两个架构。

sow config check
sow config show --all -r infra

3. 一次性导入并构建

先更新 Desired Membership,最后只构建一次:

cd ~/repo
sow add ~/repo/packages/rpm --recursive -r infra -d rpm --skip
sow add ~/repo/packages/deb --recursive -r infra -d deb --skip
sow build -r infra -d rpm -d deb
sow check -r infra

sow check 返回 0,才算初始化完成。再核对 SOW 从包头读出的真实格式与架构:

sow ls -r infra -d rpm -d deb --json |
  jq -r '.result.packages | group_by(.format + "/" + .canonical_arch)[] |
    "\(.[0].format)\t\(.[0].canonical_arch)\t\(length) packages"'

预期至少出现:

deb     aarch64   ... packages
deb     x86_64    ... packages
rpm     aarch64   ... packages
rpm     x86_64    ... packages

SOW 使用规范化架构名,因此 DEB 的 amd64/arm64 在这里显示为 x86_64/aarch64

4. 看懂生成的目录

打印实际目录:

find ~/repo -maxdepth 6 -type d | LC_ALL=C sort

关键结构应当是:

~/repo/
├── sow.yml                            # 配置,不对外服务
├── .sow/                              # 数据库、锁、恢复状态,不对外服务
├── packages/                          # 原始输入包池,可自行归档
│   ├── rpm/
│   └── deb/
└── infra/                             # 完整的公开 Repository Root
    ├── pool/                          # RPM 与 DEB 共享的单副本包池
    └── dists/
        ├── rpm/
        │   ├── x86_64/repodata/
        │   └── aarch64/repodata/
        └── deb/
            ├── Release
            └── main/
                ├── binary-amd64/
                └── binary-arm64/

这里有一个容易混淆、但必须记住的路径规则:SOW 的 Dist 固定放在 dists/ 下。因此逻辑上的 infra/rpminfra/deb,真实视图路径分别是 /infra/dists/rpm//infra/dists/deb/; 包体则统一放在 /infra/pool/。发布或挂载时必须使用完整的 ~/repo/infra,不能只拿走某个 Dist。

5. 用 Nginx 只读服务仓库

使用官方 nginx:alpine 镜像,把 Repository Root 只读挂载到 /infra

docker network create --internal infra-lab
docker run --detach \
  --name infra-nginx \
  --network infra-lab \
  --publish 8080:80 \
  --volume "$HOME/repo/infra:/usr/share/nginx/html/infra:ro" \
  nginx:alpine

直接检查两种索引入口:

curl -fsS http://127.0.0.1:8080/infra/dists/rpm/x86_64/repodata/repomd.xml | head
curl -fsS http://127.0.0.1:8080/infra/dists/deb/Release | head

Nginx 只看得到 ~/repo/infra,既看不到 sow.yml.sow/,也无法修改仓库。

6. 在 EL9 中只用 infra 安装 RPM

下面的 Rocky Linux 9 容器位于 --internal 网络中。脚本先删除所有预置仓库,再只启用刚创建的 infra,因此成功安装不能依赖公网软件源。

docker run --rm --interactive --network infra-lab rockylinux:9 bash -s <<'ROCKY'
set -euxo pipefail

rm -f /etc/yum.repos.d/*.repo
cat >/etc/yum.repos.d/infra.repo <<'REPO'
[infra]
name=Pigsty Infra RPM
baseurl=http://infra-nginx/infra/dists/rpm/$basearch/
enabled=1
gpgcheck=0
repo_gpgcheck=0
REPO

dnf clean all
dnf --disablerepo='*' --enablerepo=infra makecache
dnf --disablerepo='*' --enablerepo=infra install -y pg-exporter
rpm -q --qf '%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n' pg-exporter
command -v pg_exporter
ROCKY

RPM 的 baseurl 必须落到具体架构视图。$basearch 会由 dnf 展开为 x86_64aarch64

7. 在 Ubuntu 24.04 中只用 infra 安装 DEB

APT 的 URI 指向 Repository Root,Suites 才是 Dist 名 deb

docker run --rm --interactive --network infra-lab ubuntu:24.04 bash -s <<'UBUNTU'
set -euxo pipefail

rm -f /etc/apt/sources.list
rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources
cat >/etc/apt/sources.list.d/infra.sources <<'SOURCE'
Types: deb
URIs: http://infra-nginx/infra
Suites: deb
Components: main
Trusted: yes
SOURCE

apt-get clean
apt-get update
apt-get install -y --no-install-recommends pg-exporter
dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n' pg-exporter
command -v pg_exporter
UBUNTU

本教程使用隔离 HTTP 仓库,所以临时关闭了验签。正式服务应配置 RPM/APT 元数据签名,并移除 gpgcheck=0Trusted: yes

Docker 默认验证宿主机架构。若要完成四格运行矩阵,分别给两条 docker run 增加 --platform linux/amd64--platform linux/arm64 后各跑一次;跨架构运行需要 Docker 的 binfmt/QEMU 支持。仓库清单检查与客户端安装检查是两个独立门禁。

8. 日常维护:添加一个新版本

更新仓库的正常动作是 add,不是先删除旧包。假设已经拿到新版 pg-exporter 的四个包体:

cp ~/pgsty/infra-pkg/dist/rpm/pg-exporter-*.rpm ~/repo/packages/rpm/
cp ~/pgsty/infra-pkg/dist/deb/pg-exporter_*.deb ~/repo/packages/deb/

cd ~/repo
sow add ~/repo/packages/rpm/pg-exporter-*.rpm -r infra -d rpm --skip
sow add ~/repo/packages/deb/pg-exporter_*.deb -r infra -d deb --skip
sow build -r infra -d rpm -d deb
sow check -r infra

因为 latest Dist 配了 limit: 1,新版本胜出后,旧版本会自动退出该 Dist 的 Desired Membership。 旧字节不会被立即删除,也不会因为以后放宽策略而自动回来。

可以重新运行第 6、7 节的客户端,先 makecache/update,再安装或升级,完成更新验收。

删除只用于硬订正

正常发布不要先 sow rm。如果某个错误包必须紧急撤回,先用 sow ls -r infra -d rpm --json 或对应的 -d deb 找到精确 SHA-256, 再依次执行 sow rm sha256:... -r infra -d rpm --check 与不带 --check 的同一命令。 rm 只删除 Dist Membership,pool 字节仍由保守的 sow gc 独立回收;不要用裸包名误删所有版本与架构。

9. 两层保留策略:latest 与 stable

rpmdeblimit: 1 适合持续滚动,但不能表达“保留所有正式发布历史”。为此再创建两个 Dist:

cd ~/repo
sow dist new rpm-stable --format rpm -r infra
sow dist new deb-stable --format deb -r infra
sow config show --all -r infra -d rpm-stable -d deb-stable

新 Dist 的默认 limit0,表示保留所有版本。最终策略是:

Dist 格式 limit 角色
rpm RPM 1 RPM latest
deb DEB 1 DEB latest
rpm-stable RPM 0 累积所有已晋升 RPM
deb-stable DEB 0 累积所有已晋升 DEB

注意:stable 不是把“包池里的所有历史”自动放回来,而是从现在开始,累积每次明确晋升的版本。

10. 把 latest 晋升到 stable

SOW 没有独立的 promote 命令。当前可靠做法是先冻结写入并导出源 Dist 的精确 Membership, 再把这些对象加入目标 Dist。输入直接取自 infra/pool;SOW 会校验并复用已有 Package Object, 不会重新打包,也不会在 pool 中复制第二份包体。

先确保源状态干净,并保存本次晋升清单:

cd ~/repo
sow check -r infra
mkdir -p ~/repo/manifests

sow ls -r infra -d rpm --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/rpm-latest-202608.list
sow ls -r infra -d deb --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/deb-latest-202608.list

在晋升结束前暂停对 rpmdeb 的写入,然后复用这些 pool 对象:

cd ~/repo
(
  set -e
  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d rpm-stable --skip
  done < ~/repo/manifests/rpm-latest-202608.list

  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d deb-stable --skip
  done < ~/repo/manifests/deb-latest-202608.list

  sow build -r infra -d rpm-stable -d deb-stable
  sow check -r infra
)

每次 add 应报告 reused。若中途失败,源 Dist 不受影响;修复问题后对同一清单重跑即可。 随着以后重复晋升,rpm/deb 仍只保留最新版本,而 rpm-stable/deb-stable 会逐次累积历史版本。

11. 从 stable 创建 2026-08 快照

客户端可见的月度快照也是两个新的 Dist:

cd ~/repo
sow dist new rpm-202608 --format rpm -r infra
sow dist new deb-202608 --format deb -r infra
sow config show --all -r infra -d rpm-202608 -d deb-202608

在快照窗口内暂停 stable 写入,先把其精确 Membership 固化为清单:

sow check -r infra
sow ls -r infra -d rpm-stable --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/rpm-stable-202608.list
sow ls -r infra -d deb-stable --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/deb-stable-202608.list

再把清单加入对应快照 Dist:

cd ~/repo
(
  set -e
  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d rpm-202608 --skip
  done < ~/repo/manifests/rpm-stable-202608.list

  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d deb-202608 --skip
  done < ~/repo/manifests/deb-stable-202608.list

  sow build -r infra -d rpm-202608 -d deb-202608
  sow check -r infra
)

把这次已验证的完整 Repository Generation 也加入保留集合,防止后续 GC 把它当作不可达历史处理:

sow retain add "$(sow status -r infra --json | jq -r '.result.built_generation')" -r infra
sow retain ls -r infra

retain 保留的是整个 Repository Generation,用于恢复与 GC 安全根;客户端可见的固定 URL 仍由 rpm-202608deb-202608 两个 Dist 提供。SOW 尚不强制快照 Dist 只读,因而创建后不再对它们 执行 addrm 是维护规约的一部分。

12. 客户端地址总表

同一个 Nginx 与同一份 infra/pool 支撑所有 channel:

Channel dnf baseurl APT URIs / Suites
latest http://infra-nginx/infra/dists/rpm/$basearch/ http://infra-nginx/infra / deb
stable http://infra-nginx/infra/dists/rpm-stable/$basearch/ http://infra-nginx/infra / deb-stable
2026-08 http://infra-nginx/infra/dists/rpm-202608/$basearch/ http://infra-nginx/infra / deb-202608

最终验收:

cd ~/repo
sow dist ls -r infra
sow status -r infra
sow check -r infra

到这里,我们得到的不是一次性演示目录,而是一个可以继续收包、晋升与做月度快照的真实 Infra Repository:rpm/deb 负责快速更新,rpm-stable/deb-stable 负责积累正式历史,月度 Dist 提供固定入口, 所有视图复用同一份不可变包体。

实验结束后可停止临时服务:

docker rm --force infra-nginx
docker network rm infra-lab