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 yieldlayers == []and a niltotal_size, and image manifests still return their real layers and size. A manifest-list body that containslayers/configsubstrings yields the same null result with no error, pinning theis_a?(Hash)guards against theString#[]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 nulltotalSizewith no errors (previously 500).
Closes #603051
Context for AI agents
- Rejected alternative (the issue's original proposal): narrow the default
faradayAccept and restore a wideACCEPTED_TYPES_RAWforfaraday_raw. This regresses #600486 (closed). Geo's digest enumeration uses the defaultfaraday(repository_tag_digest), notfaraday_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_manifestviaallow_next_instance_of(ContainerRegistry::Client)so the realTag#layersruns. The new examples should fail (500 / GraphQL error) with thetag.rbchange 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 before6975aa6e9a73(no crash, out of scope). The REST schema rendersrevision,created_at, andtotal_sizeas null for config-less tags, already true for v1 tags; theconfig_blobguard now makes that hold for fat manifests too, where aString#[]substring match on a manifest-list body could otherwise build a bogus blob and raise.