Repository navigation
Docker / OCI as a FormatPlugin: an image store in the plugin contract #447
Description
After #440 (#437), every format is a built-in crate and the contract is Rust only. After #441 (#431), raw is a FormatPlugin over a host file store (storage = "files"). Docker / OCI is the last native format. #445 moves its crate to crates/formats/docker as it is and leaves this to its own issue. This is that issue.
The split is the one raw got. The format crate owns the distribution-spec protocol. The host owns the image store, authentication and the token service. The plugin never sees credentials, never touches storage and never holds a blob in memory.
Where things stand
forge-oci (about 7,400 lines after #435) holds both halves, behind Registry::handle and its own /v2 routes (crates/server/src/registry.rs):
| Protocol (moves to the format) | Image store and auth (stays with the host) |
|---|---|
path.rs: endpoints parsed from the end of /v2/… |
token.rs, auth.rs: /v2/token, registry Bearer tokens, workload liveness |
digest.rs, reference.rs: digest, tag and name grammar |
service.rs: site (by path or by config.host), caller, RBAC, selectors, token scopes, a group's writable member |
manifest.rs: manifests, indexes, artifacts, subject |
push.rs: upload sessions, chunks, cross-repository mounts, manifest commits and their references, immutable tags, the trusted-publishing ledger |
error.rs: OCI error codes and bodies |
pull.rs, group.rs: manifests, blobs, tags and referrers of hosted, proxy and group repositories |
chart.rs (#435): Helm chart metadata, README and values |
proxy.rs: upstream fetches, Bearer token exchange with the upstream (Docker Hub), caching; gc.rs; import.rs (Nexus import) |
The oci_* tables (migration 0033) and their reference rules stay as they are.
Proposal
Storage images. The format's descriptor (#444) says storage: Images. The host keeps the image store as a server module next to the file store (crates/server/src/images, from the right column above). GET /admin/formats lists Docker from its descriptor, as it lists raw. DOCKER_FORMAT / is_docker() checks become checks for images storage, as #431 did for files.
Routing stays /v2. #438 keeps Docker at /v2/. The host keeps:
/v2/,/v2/tokenand/v2/_catalog;- by-path and by-host resolution of
<name>; - authorization: the Bearer challenge and scopes, RBAC on the image, the write target.
The endpoint grammar is the format's, but authorization depends on it (an upload's GET needs write). So the contract gets one function for images formats that splits a path into the name and the endpoint with its action. The host resolves and authorizes with that, then calls handle with the repository, the image and the rest of the path.
Contract additions (forge-plugin-api, images formats only, like the file queries):
-
Reads:
Manifest { image, reference }: digest, media type, size, artifact type, subject, the stored summary;Blob { image, digest };Tags { image, last, n };Referrers { image, digest, artifact_type }.
-
Changes, each alone in its round:
StartUpload { image, mount: Option<(from, digest)> };UploadStatus(id);AppendUpload { id, range }: the request body;FinishUpload { id, digest };CancelUpload(id);PutManifest { image, reference }: the request body;DeleteManifest { image, reference };DeleteBlob { image, digest }.
-
Actions:
ServeBlob { image, digest }andServeManifest { image, reference }. The host streams them with oneRange,Docker-Content-DigestandETag. -
parse_manifest(bytes) -> ParsedManifest, likeparse_package. The host stages a pushed manifest (at most 4 MiB) and asks the format for:- its media type;
- the blobs and manifests it names;
- its subject, artifact type and platform;
- a summary.
The host checks that the named blobs exist and commits the manifest. A proxy's manifests go through the same function. Helm chart metadata (Serve Helm charts from the OCI registry only, with a chart page in the web UI #435) becomes part of the summary. Reading a chart's README and values for the web UI is a format function over a blob the host lends, as
parse_packagegets a package. -
OCI errors are the format's. A failed change comes back to
renderas a typed error (blob unknown, digest invalid, manifest blob unknown, size invalid, tag immutable, …), not as the host's own response. The format renders the OCI JSON body and status. Registry clients read the error codes, so this differs from files. -
Upstream operations:
Manifest { image, reference }(a URL and theAcceptmedia types),Blob { image, digest }(a URL) andTags { image, last, n }. The host sends them and handles a401with a Bearer challenge by fetching an upstream token (the upstream client's concern, as credentials are now). The format names the URLs and parses what comes back. This lifts two limits listed indocs/format-plugins.md: fetches without headers, and no token exchange.
What stays out of the format. The token service and its signing keys. The registry's response hardening (Content-Security-Policy, nosniff, Docker-Distribution-API-Version on every answer). GC. The Nexus import, which commits images through the host's store.
Out of scope
- URLs: Docker stays at
/v2/(Serve every repository at /repository/{name}/, with no format in the URL (breaking) #438), by path and by host. - New registry features, and the catalog API (still refused).
- Renaming the plugin vocabulary (Format crates under crates/formats/, each described in Rust instead of plugin.toml #442 leaves that open).
Acceptance criteria
-
crates/formats/dockerimplementsFormatPluginwith images storage. The distribution-spec protocol (paths, grammar, manifests, errors, charts, response headers, upstream URLs) lives there and nowhere else. - The image store is a host module. The server holds no
/v2protocol logic beyond routing, authorization and the token service. -
crates/server/tests/registry.rspasses unchanged. -
.github/scripts/oci-e2e.shpasses:- the OCI conformance suite, buildx, crane and cosign;
- a
registry:2upstream through a proxy and a group; - Helm push, pull and install (Serve Helm charts from the OCI registry only, with a chart page in the web UI #435).
- The
dockerandimage-smokejobs pass. -
GET /admin/formatslists Docker from its descriptor withstorage: images, and no host check names thedockerid. -
docs/format-plugins.mdgets an Images formats section in place of Why Docker is not a plugin.docs/docker.mdand theAGENTS.mdrule on formats no longer call Docker an exception.
Order
Start after #441, #444 and #445: the crate's home, the descriptor's storage field and the files model this mirrors. One pull request, as #431 was for raw.
Activity