pnpm pnpr 的 OCI 容器镜像服务:用 docker / podman / skopeo 向 pnpr 推送镜像
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
导读
本指南讲解 pnpm 项目新引入的 pnpr(pnpm 的 Rust 实现)容器镜像服务能力:只需在注册表配置中声明ecosystem: oci,pnpr 就会以标准 OCI Distribution 协议对外提供镜像仓库,可用docker、podman、skopeo直接推送与拉取。读完本文,你将掌握如何在 pnpr 中声明 OCI 注册表、用docker login+ pnpr token 完成认证、理解/v2/API 与镜像命名规则,并深入看到这一功能背后的 OCI 协议实现(manifest、digest、镜像文档、referrers 等)。
功能总览:pnpr 从包注册表到镜像仓库
在 pnpm 仓库的变更说明 .changeset/serve-container-images.md 中,@pnpm/pnpr以minor版本引入了如下能力:
pnpr now serves container images. Declare a registry with
ecosystem: oci. Push to it withdocker,podman, orskopeo. The distribution API answers at/v2/on the host root. An image keeps its own name, with no registry key in the path. Sign in withdocker login, using a pnpr token as the password.
翻译并展开,这一变更意味着:
- pnpr 不仅仅能代理 npm / cargo / pypi 等软件包生态,还能直接充当容器镜像仓库;
- 注册表通过
ecosystem: oci声明,纳入 pnpr 统一的注册表路由与访问控制体系; - 服务端实现的是标准OCI Distribution Specification,因此 Docker 生态的任意客户端(
docker、podman、skopeo)都能开箱即用; - 认证复用 pnpr 自身的 token 体系,用户不需要额外引入独立的镜像仓库服务。
OCI 协议层的纯数据实现集中在 pnpr/crates/oci/src/lib.rs,其模块注释明确写道:这是"pnpr 所讲的 OCI distribution 协议"(The OCI distribution protocol pnpr speaks),包含三部分:命名所有 blob 的Digest、客户端推送的Manifest形状,以及托管注册表为每个仓库保存的ImageDocument。
配置:如何声明一个ecosystem: oci的注册表
配置项解析
在 pnpr/crates/config/src/config_file.rs 中,hosted(托管型注册表)与upstream(上游代理型注册表)都带有一个ecosystem字段,注释说明:该字段决定此注册表服务的包生态,进而选择它使用的协议。当取值是oci时,pnpr 就以 OCI Distribution 协议对外服务。
从 pnpr/crates/config/src/registry_graph.rs 的生态组(ecosystem group)展开逻辑看,配置既可以采用"组"语法,也可以采用"单条声明"语法:
- 组语法:
oci:组下的所有注册表自动继承ecosystem: oci,并且org(存储归属)默认取oci~<name>,defaultRegistry等默认路由也会带上oci/前缀; - 单条语法:显式写
ecosystem: oci。
例如 pnpr/crates/config/src/tests/registry_graph.rs 中的测试就使用了ecosystem: oci的 hosted 注册表,并验证resolve_default(Ecosystem::Oci, repository)的路由行为;pnpr/crates/config/src/tests/storage.rs 则验证了生态组的名称作用域、默认注册表与存储组织规则。
一个最小可用的声明大致如下(以测试中出现的生态组语法为基础):
# pnpr 配置文件(config.yaml) registries: oci: images: # 注册表名,存储 org 默认成为 oci~images type: hosted ecosystem: oci # 组内可省略,此处显式写出更清晰说明:具体的
access/packages权限规则沿用 pnpr 统一的注册表访问控制模型,ecosystem: oci的键名同样会经过生态相关的规范化校验(见 pnpr/crates/config/src/registry_graph/namespace.rs)。
协议选择如何影响路由
在 pnpr/crates/config/src/registry_graph.rs 中,Npm之外的生态会被逐一登记进路由表,oci作为独立生态获得自己的默认注册表与来源解析;pnpr/crates/config/src/lib.rs 中还提供了独立的OciConfig(位于http.oci下),其中包含max_manifest_bytes、max_blob_bytes等大小限制,供服务端在读取 manifest / blob 请求体时做上限校验。
推送容器镜像:docker / podman / skopeo
配置好ecosystem: oci的注册表后,即可用任意 OCI 兼容客户端推送镜像。以docker为例:
# 1. 登录(密码使用 pnpr token,见下文认证一节) docker login pnpr.example.com # 2. 给本地镜像打上 pnpr 仓库的 tag docker tag my-app:1.0.0 pnpr.example.com/my-app:1.0.0 # 3. 推送 docker push pnpr.example.com/my-app:1.0.0podman、skopeo用法等价:podman push pnpr.example.com/my-app:1.0.0,skopeo copy docker-daemon:my-app:1.0.0 docker://pnpr.example.com/my-app:1.0.0。
推送过程本质上是分两步完成的,这与 pnpr 的存储模型深度绑定(见 pnpr/crates/oci/src/lib.rs 的模块注释):
- 上传 blob(层与配置):镜像的 config 与每一层 layer 都是内容寻址、不可变的字节块,客户端先把它们上传到
blobs/uploads/; - 写入 manifest(发布提交点):上传完成后客户端
PUT一份 manifest,引用这些 blob 的 digest。只有 manifest 写入才算发布了一个版本——没有任何 manifest 引用的 blob 只是待回收的垃圾,而不是"发布了一半的版本"。
镜像清单的形态校验
pnpr 在 pnpr/crates/oci/src/manifest.rs 定义了接受的 manifest 结构:schemaVersion(仅接受 2,Docker schema 1 早已废弃而被拒绝)、mediaType、config(一个 Descriptor)、layers、manifests(用于镜像索引)、subject(referrer 机制)、artifactType与annotations。校验规则(manifest.rs)包括:
schemaVersion必须是 2;mediaType未声明时回退到请求头Content-Type,再回退到默认的 OCI image manifest;- 非索引类 manifest 必须携带
configdescriptor; - 带
subject的 referrer 若本身不是索引,则必须携带artifactType或可推导的 config mediaType。
支持的媒体类型
为了兼容"大多数客户端仍然推送 Docker 旧类型"的现实(见 pnpr/crates/oci/src/media_type.rs),pnpr 同时接受 OCI 与 Docker 两类 manifest 媒体类型(lib.rs):
| 媒体类型 | 含义 |
|---|---|
application/vnd.oci.image.manifest.v1+json | OCI 镜像 manifest |
application/vnd.oci.image.index.v1+json | OCI 镜像索引 |
application/vnd.docker.distribution.manifest.v2+json | Docker 镜像 manifest |
application/vnd.docker.distribution.manifest.list.v2+json | Docker manifest 列表 |
命名与路径:/v2/与"镜像自带名字"
Distribution API 挂载点
OCI Distribution 协议要求所有端点位于镜像引用主机名下的/v2/路径。在 pnpr/crates/oci/src/lib.rs 中,这个段被定义为常量:
/// The path segment every distribution endpoint sits under. pub const API_SEGMENT: &str = "v2";客户端根据镜像引用的 host 推导出 API 地址,因此该段"不能移动或重命名"。pnpr/crates/pnpr/src/server/oci/tests.rs 的测试验证了这一点:例如api_base("/v2/acme/app/manifests/1.0", ...)解析出的 API 根就是/v2。
镜像名中不含 registry key
变更说明特别强调:"An image keeps its own name, with no registry key in the path." 也就是说,镜像引用形如pnpr.example.com/acme/app:1.0.0,路径中的仓库名是acme/app,不会把注册表的 key(如oci/images)塞进镜像名。registry key 只影响 pnpr 内部的路由与存储归属(org默认为oci~<name>),不影响对外协议层的命名。
服务端对/v2/<name>/<type>/<reference>的路由实现位于 pnpr/crates/pnpr/src/server/oci/manifest_request.rs(HEAD/GET/PUT/DELETE四种方法的 manifests 端点)与 pnpr/crates/pnpr/src/server/oci/discovery.rs(GET /v2/_catalog仓库列表、GET /v2/<name>/tags/list标签列表)。
标签与引用规则
pnpr/crates/oci/src/lib.rs 实现了 distribution 规范的 tag 语法[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}:
- 最大长度
MAX_TAG_LEN = 128; - 首字符必须是 ASCII 字母数字或下划线;
- 后续字符仅允许字母数字、
.、_、-。
manifest 引用只有两种形态:tag 或 digest,除此之外一律拒绝——既防止存下任何符合规范的客户端都无法寻址的元数据,也避免sha256:short这类"长得像 digest"的 tag 混入标签列表。
标签是仓库里唯一的可变对象
pnpr/crates/oci/src/document.rs 对TagEntry的设计值得关注:每个 tag 记录tag、指向的digest以及updated(Unix 毫秒时间戳)。标签是仓库中唯一可变的东西,因此"已存在即优先"的不可变合并规则不适用于 tag——崩溃恢复后重放旧事务会把 tag 拖回旧 manifest。改为按时间戳比较后合并是单调的:旧写入重放变成空操作,同毫秒平局时新写入获胜(因为同一仓库的活跃写入被其包锁串行化)。这为跨副本、崩溃恢复场景下标签的最终一致性提供了保证。
认证:docker login+ pnpr token
OCI 客户端通过标准的 registry 认证流程登录,pnpr 复用其自身的 token 体系:
docker login pnpr.example.com # Username: <你的 pnpr 用户名> # Password: <你的 pnpr token>变更说明明确:密码处填 pnpr token。服务端的认证接入点在 pnpr/crates/pnpr/src/server/authentication.rs:请求会先尝试按 OCI token 解码(pnpr/crates/pnpr/src/server/oci/tokens/ 模块),解码失败则走常规认证路径;同文件第 346 行 通过检查路径段中是否出现pnpr_oci::API_SEGMENT(即v2)来识别 OCI 请求。tokens/tests.rs 中的测试展示了 token 的作用域模型:claims 携带audience(如/oci/~images)与scopes(如acme/v2/app上的pull/push权限),随后按路径与 HTTP 方法逐条判定是否放行。
深度实现:pnpr 如何存储一个镜像仓库
ImageDocument:仓库的持久化形状
pnpr/crates/oci/src/document.rs 定义了每个镜像仓库的持久化文档ImageDocument,包含:
name:仓库名;manifests:按 digest 排序的 manifest 条目(ManifestEntry记录 digest、mediaType、size,以及可选的 referrer 元数据);tags:按标签名排序的TagEntry;generation:代际计数,用于隔离"显式 blob 删除之前已预备的日志化发布";deleting_blob:待删除的 blob digest,删除完成前会阻止新的发布。
层与配置 blob 刻意不记录在文档里:它们是内容寻址、不可变、且在有人引用之前就先上传的字节;manifest 才是发布动作,因此文档只记录 manifest 与 tag。两个集合始终保持有序,因为这里的所有查找都是二分查找(manifest()、tag()、resolve()均如此)。
合并语义:让事务重放安全
merge()(document.rs)用于把日志化写入合并进已存储文档:manifest 内容寻址,已存在即同字节、保持不变;tag 按时间戳单调合并。generation 不匹配或存在待删除 blob 时拒绝合并。这样,被新写入取代的旧日志化写入可以安全重放而不破坏状态。
延伸能力:referrers、分页、blob 删除与在线回收
围绕容器镜像服务,仓库中还有多个配套的变更说明与实现,可作为实战参考:
- Referrers API:
GET /v2/<name>/referrers/<digest>列出挂接到某镜像上的工件(签名、SBOM、扫描结果等),支持artifactType过滤,实现位于 pnpr/crates/pnpr/src/server/oci/referrers.rs,并在.changeset/pnpr-oci-protocol-surface.md中说明; - 跨仓库 blob 挂载与分段下载:跨仓库 blob 挂载复用可读仓库中的层;支持 Range 请求下载 blob;
_catalog与tags/list支持n/last分页(见 discovery.rs 与.changeset/pnpr-oci-protocol-surface.md); - 删除与在线 GC:
DELETE /v2/<name>/manifests/<reference>由 deletion.rs 实现,与 manifest_request.rs 的删除端点配合,通过deleting_blob门闩阻止删除期间的新发布; oci-gc离线回收:pnpr oci-gc --registry <name>收集旧的无引用镜像 blob(要求先停止该注册表的写入),--dry-run可先预览;核心逻辑在 pnpr/crates/pnpr/src/oci_maintenance.rs,命令入口在 pnpr/crates/pnpr/src/main.rs,见.changeset/oci-registry-operations.md;- 跨副本上传恢复:使用 S3 存储时,跨副本的上传会话可恢复,24 小时无活动的废弃共享上传会话在启动时被清理(
.changeset/oci-registry-operations.md)。
小结
从.changeset/serve-container-images.md出发可以看到,pnpr 的容器镜像服务不是"另起炉灶",而是把 OCI Distribution 协议作为一类新的生态(ecosystem: oci)接入其既有的注册表路由、访问控制、token 认证与存储体系。对外它是标准的/v2/镜像仓库,docker/podman/skopeo开箱即用;对内它依靠内容寻址 blob、以 manifest 写入为发布提交点、用时间戳单调合并保证标签的崩溃安全。无论你是想用 pnpr 统一托管内部 npm 包与容器镜像,还是希望深入理解 OCI 注册表的工程实现,都可以从 pnpr/crates/oci 与 pnpr/crates/pnpr/src/server/oci 这两个目录开始阅读源码。
【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考