Fix 500 on multi-arch tags on the legacy registry path

Why

Since 19.0.2, the container registry UI and tag API return 500 for multi-arch tags on the legacy (non-database) path. The tag API (GET /projects/:id/registry/repositories/:rid/tags/:tag) and the GraphQL totalSize field both fail, while docker push and pull keep working.

Commit 6975aa6e9a73 widened the shared ACCEPTED_TYPES Accept header to include the fat-manifest media types, fixing Geo OCI-index sync (#600486 (closed)). The registry then returns the real manifest list or OCI image index for a multi-arch tag instead of a downgraded single-platform manifest. ContainerRegistry::Tag#layers assumed an image manifest and ran nil.map on the absent layers, raising NoMethodError.

This change hardens the parser rather than narrowing the Accept header. The wide Accept is load-bearing for Geo: its digest enumeration reads the index digest through the default faraday connection (repository_tag_digest), not faraday_raw, so narrowing the Accept would regress that sync fix. Metadata-database installs were never affected, since the v1 API client sets tag size from the API and never parses a manifest.

What

Tag#layers returns [] for a manifest list (which arrives as an unparsed String) or an OCI image index (a Hash with no layers), and Tag#total_size returns null for both. Multi-arch tags on the legacy path now show a blank size instead of returning 500. Tag#config_blob takes the same is_a?(Hash) guard, so revision and created_at also return null for a fat manifest rather than building a bogus blob from a String#[] substring match. Computing a real size there needs a per-platform manifest fan-out, the slow enumeration the database API already replaces, on a path being retired.

Test plan

  • spec/lib/container_registry/tag_spec.rb: a manifest list and an OCI image index each yield layers == [] and a nil total_size, and image manifests still return their real layers and size. A manifest-list body that contains layers/config substrings yields the same null result with no error, pinning the is_a?(Hash) guards against the String#[] substring footgun.
  • spec/requests/api/project_container_repositories_spec.rb: the tag API returns 200 with a blank size for a multi-arch tag on the legacy path (previously 500).
  • spec/requests/api/graphql/container_repository/container_repository_details_spec.rb: the tags query returns a null totalSize with no errors (previously 500).

Closes #603051

Context for AI agents
  • Rejected alternative (the issue's original proposal): narrow the default faraday Accept and restore a wide ACCEPTED_TYPES_RAW for faraday_raw. This regresses #600486 (closed). Geo's digest enumeration uses the default faraday (repository_tag_digest), not faraday_raw, so the index digest would again be nil and Geo would silently skip OCI-index tags. The fix therefore changes no Accept header.
  • Why blank size and not a computed one: a real multi-arch size needs a per-platform sub-manifest fan-out, the v2-by-tag enumeration the metadata database replaces, and the legacy path is being retired.
  • Reviewer check (fail-first): the request specs stub repository_manifest via allow_next_instance_of(ContainerRegistry::Client) so the real Tag#layers runs. The new examples should fail (500 / GraphQL error) with the tag.rb change reverted. They were authored but not run against a reverted tree locally, so CI is the first full run.
  • Non-goals: the database-path GraphQL manifest(reference:) field now returns a fat manifest where it returned a downgraded one before 6975aa6e9a73 (no crash, out of scope). The REST schema renders revision, created_at, and total_size as null for config-less tags, already true for v1 tags; the config_blob guard now makes that hold for fat manifests too, where a String#[] substring match on a manifest-list body could otherwise build a bogus blob and raise.
Edited by Hayley Swimelar

Merge request reports

Loading