Amend ADR-009: container tag listing and referrers unavailable on virtual repositories

Why is this change being made?

ADR-009 marks only write routes unavailable on a virtual repository. The S32 container-virtual spec requires GET .../tags/list and GET .../referrers/<digest> to answer 404 NAME_UNKNOWN on a virtual container repository (its ### Tag listing, ### Referrers, and ## API Contracts sections), so today that behavior is a divergence with nothing in the ADR on record. The spec's ## Follow-ups tracks this amendment under artifact-registry#264.

This MR gates no merge. It gates opening Step 13 of the S32 container-virtual plan, because that step's MR description has to cite an amendment URL that resolves. The MR recording both amendment URLs for the plan is artifact-registry!2063.

This introduces a new annotation class: read-route unavailability

Every unavailability annotation in ADR-009 today is on a write route and reads (not available for remote and virtual repositories). This MR adds the first read-route unavailability, and it deliberately does not reuse that phrase.

The new phrase is (not available for virtual repositories in the MVP). It differs in two ways that matter, and a reader who assumes the two phrases are the same thing will misread every future use of the new one:

  • It names virtual alone, not remote. Both routes are available on a remote container repository: S16 ships them as live proxies against the upstream, with the body's name field and the Link header rewritten, and that code is merged. Reusing the write-route phrase would mark shipped behavior unavailable and put the ADR in conflict with the running service.
  • It says "in the MVP". The write-route unavailability is architectural — a virtual repository has nothing to write to. This one is a scope limit that a follow-up retires.

What changed

1. Four route lines annotated, not two

Each route appears twice in the list — the plain form and the query-parameter form:

Route Description
GET .../tags/list List all tags in repository
GET .../tags/list?n=100&last=tag_name Paginated tag listing
GET .../referrers/:digest List artifacts/attestations referencing a manifest
GET .../referrers/:digest?artifactType=<type> Filter referrers by artifact type

All four now carry the annotation. Annotating only the two plain forms would leave the paginated and artifact-type-filtered halves reading as available on a virtual repository — and the paginated line is the one a reader checking pagination behavior lands on.

2. The section's introductory parenthetical widened

It read:

The registry resolves the repository kind internally and applies type-specific behavior (e.g., rejecting writes on remote and virtual repositories).

Describing type-specific behavior as writes-only while four read routes carry unavailability annotations would leave the document inconsistent with its own route list. The parenthetical now also covers refusing reads a repository kind cannot serve today — the qualifier matters, for the reason under item 3.

3. A note recording the status code, the client split, and the scope

The annotation stays in the route list's register, so the reasoning goes in a note below the list. It is four paragraphs rather than the two bullets an earlier revision of this MR carried, because the client split needed stating.

The status is 404 NAME_UNKNOWN, not 405, and not an empty 200. A 200 with an empty list would tell the client that no referrers exist, on evidence no upstream supplied.

Which OCI clients take the referrers tag-schema fallback turns on the error code. crane reads no error code at all: it falls back on a 404, 400, or 406, or on a 200 whose Content-Type is not an OCI index, and returns any other status as an error. oras parses the body and returns the error when the code is NAME_UNKNOWN; any other code on the same 404 sends it to the tag schema. Notation inherits that through oras-go. Measured against go-containerregistry v0.20.6, oras-go v2.5.0 and v2.6.0, and notation-go v1.3.2, which pins oras-go v2.5.0.

The OCI error set has no code for a repository that exists and has no collection to list, so NAME_UNKNOWN is reused rather than chosen. Two costs follow: discovery tooling reads it as "no such repository", so crane ls, skopeo list-tags, and registry UIs report a virtual repository as absent while pulls against the same URL succeed; and the fallback that motivates preferring a 404 is unavailable to exactly the clients that read the code.

The unavailability is MVP-scoped, not architectural. S13's resolution model resolves one path across upstreams and has no operation for merging a paginated collection across N upstreams with independent cursors. The 404-over-405 argument is therefore worded as "no single collection the resolution model can produce today" rather than "no collection at all", which would be a permanent claim the scope paragraph four lines below contradicts.

4. The cost joins ### Negative

## Consequences is where a reader goes for the costs of the decisions this document makes, and the discovery-tooling misreport was recorded only inside the route note about 100 lines above it. One bullet there now points back at the note.

Two work items, and which is which

  • #264 tracks the cross-upstream merge that replaces the 404 with a real listing. Once that lands the route answers 200 and the error code is moot.
  • #1019 tracks the error code itself, filed for this review. A 404 carrying any code other than NAME_UNKNOWN keeps the tag-schema fallback working in oras and Notation as well as crane, and Step 13 writes the route before #264 (closed)'s merge design exists. An earlier revision pointed the code question at #264 (closed), where a reader lands on the 404 recorded as "deliberate rather than incidental".

tags/list is unaffected by that decision either way: neither library has a tag-listing fallback at any status.

Scoped to container; no sibling amendment

No npm or Maven route needs the same treatment. The npm virtual slice serves its dist-tags read on a virtual repository, and the npm read route it does refuse is not listed in ADR-009 at all.

Follow-on outside this MR

artifact-registry!2127 carries the same client qualification into the S32 spec, at the four places that stated the fallback unconditionally. #264 (closed)'s own S32 referrers bullet states it unqualified too and wants the same correction.

Validation

  • markdownlint-cli2 on the changed file: clean.
  • vale --minAlertLevel error on the changed file: clean. It reports 0 errors and 9 warnings, the same count and the same rules as origin/main, so none is introduced here.
  • One file changed. No file overlap with !20928 (merged), which touches ADR-007 only. !20941 (merged) also touches ADR-009, in ##### Repository Deletion, which this MR does not go near.
Edited by Radamanthus Batnag

Merge request reports

Loading
Loading