pnpm pnpr 的 OCI 容器镜像服务:用 docker / podman / skopeo 向 pnpr 推送镜像
2026/9/20 16:37:02 网站建设 项目流程

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 协议对外提供镜像仓库,可用dockerpodmanskopeo直接推送与拉取。读完本文,你将掌握如何在 pnpr 中声明 OCI 注册表、用docker login+ pnpr token 完成认证、理解/v2/API 与镜像命名规则,并深入看到这一功能背后的 OCI 协议实现(manifest、digest、镜像文档、referrers 等)。

功能总览:pnpr 从包注册表到镜像仓库

在 pnpm 仓库的变更说明 .changeset/serve-container-images.md 中,@pnpm/pnprminor版本引入了如下能力:

pnpr now serves container images. Declare a registry withecosystem: 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 生态的任意客户端(dockerpodmanskopeo)都能开箱即用;
  • 认证复用 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_bytesmax_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.0

podmanskopeo用法等价:podman push pnpr.example.com/my-app:1.0.0skopeo copy docker-daemon:my-app:1.0.0 docker://pnpr.example.com/my-app:1.0.0

推送过程本质上是分两步完成的,这与 pnpr 的存储模型深度绑定(见 pnpr/crates/oci/src/lib.rs 的模块注释):

  1. 上传 blob(层与配置):镜像的 config 与每一层 layer 都是内容寻址、不可变的字节块,客户端先把它们上传到blobs/uploads/
  2. 写入 manifest(发布提交点):上传完成后客户端PUT一份 manifest,引用这些 blob 的 digest。只有 manifest 写入才算发布了一个版本——没有任何 manifest 引用的 blob 只是待回收的垃圾,而不是"发布了一半的版本"。

镜像清单的形态校验

pnpr 在 pnpr/crates/oci/src/manifest.rs 定义了接受的 manifest 结构:schemaVersion(仅接受 2,Docker schema 1 早已废弃而被拒绝)、mediaTypeconfig(一个 Descriptor)、layersmanifests(用于镜像索引)、subject(referrer 机制)、artifactTypeannotations。校验规则(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+jsonOCI 镜像 manifest
application/vnd.oci.image.index.v1+jsonOCI 镜像索引
application/vnd.docker.distribution.manifest.v2+jsonDocker 镜像 manifest
application/vnd.docker.distribution.manifest.list.v2+jsonDocker 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 APIGET /v2/<name>/referrers/<digest>列出挂接到某镜像上的工件(签名、SBOM、扫描结果等),支持artifactType过滤,实现位于 pnpr/crates/pnpr/src/server/oci/referrers.rs,并在.changeset/pnpr-oci-protocol-surface.md中说明;
  • 跨仓库 blob 挂载与分段下载:跨仓库 blob 挂载复用可读仓库中的层;支持 Range 请求下载 blob;_catalogtags/list支持n/last分页(见 discovery.rs 与.changeset/pnpr-oci-protocol-surface.md);
  • 删除与在线 GCDELETE /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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询