Skip to content

Docker / OCI as a FormatPlugin: an image store in the plugin contract #447

Description

@schmitch

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/token and /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 } and ServeManifest { image, reference }. The host streams them with one Range, Docker-Content-Digest and ETag.

  • parse_manifest(bytes) -> ParsedManifest, like parse_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_package gets a package.

  • OCI errors are the format's. A failed change comes back to render as 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 the Accept media types), Blob { image, digest } (a URL) and Tags { image, last, n }. The host sends them and handles a 401 with 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 in docs/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

Acceptance criteria

  • crates/formats/docker implements FormatPlugin with 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 /v2 protocol logic beyond routing, authorization and the token service.
  • crates/server/tests/registry.rs passes unchanged.
  • .github/scripts/oci-e2e.sh passes:
  • The docker and image-smoke jobs pass.
  • GET /admin/formats lists Docker from its descriptor with storage: images, and no host check names the docker id.
  • docs/format-plugins.md gets an Images formats section in place of Why Docker is not a plugin. docs/docker.md and the AGENTS.md rule 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions