fix(oci): force-stream manifest payloads to serve stored media type

Why

In DELIVERY_MODE_REDIRECT, a manifest GET returns a 307 to a signed storage URL that serves the payload as application/octet-stream, dropping the stored media type. containers/image clients (skopeo, podman, CRI-O) read the response Content-Type to detect the manifest schema, so pulls fail with unsupported schema version 2. Push and DELIVERY_MODE_PROXY pulls are unaffected: proxy mode already streams the payload with the stored media type. The OCI conformance suite passes in redirect mode, so real clients hit a gap the suite misses.

What

Force-stream manifest payloads so AR serves them with the stored container_manifests.media_type. Blobs keep redirecting. The fix adds storage.WithForceStream() to the one manifest-payload OpenBlob call in GetManifest, so the storage layer serves the bytes directly instead of signing a URL. This matches the Container Registry, which proxies manifests and redirects only blobs.

Reviewer-facing behavior change: under DELIVERY_MODE_REDIRECT, a manifest GET now returns 200 with the payload instead of a 307. Manifest payloads (≤4 MB, usually KB) now stream through AR, a negligible bandwidth change. The blob_download_bytes{delivery} label for manifest GETs moves from redirect to stream, which needs external Grafana and runbook updates (no in-repo dashboard keys reference it). The now-unreachable serveManifestRedirect is removed, and the S12 Manifest Pull wording is corrected to match invariant 5.

Test plan

  • TestGetManifest_GET_ForcesStream asserts the manifest GET resolves ForceStream==true, verified to fail against the pre-fix code.
  • Existing handler tests assert the manifest GET returns the stored media type as Content-Type.
  • The storage force-stream contract is already regression-locked in pg_open_blob_integration_test.go.
  • conformance:oci runs on the MR pipeline. The fix moves manifests toward conformance (200 plus the stored Content-Type).
Acceptance criterion Test / evidence
Manifest GET folds OpenBlob opts to ForceStream==true TestGetManifest_GET_ForcesStream (new; fails pre-fix)
Manifest GET returns 200 + body + Content-Type = stored media type existing handler content-type tests + the storage force-stream contract
Blob GET still redirects (307) under redirect mode TestBlobGet_RedirectReturns307 (unchanged)
No dead redirect code remains serveManifestRedirect removed; grep confirms zero references
S12 states manifests always stream docs/specs/S12-container-oci-hosted.md edits
Package tests, lint, OCI conformance pass local build/vet/test + CI conformance:oci

Related to #362 (closed)

Context for LLM agents

Design: chose Option B (force-stream manifests) over Option A (a response-content-type override on the signed URL). Option A works on GCS and S3 natively but requires extending the URLFor(ctx, path) storage interface, both drivers, three URL middleware decorators (CloudFront, Cloud CDN, URL cache), and CDN query-parameter forwarding config. That is a wide, signing-correctness-sensitive blast radius for a severity::2 fix. Option B is one functional line against an already-tested path (effectiveMode returns proxy when ForceStream, so URLFor is skipped), matches the Container Registry reference impl, and fits ADR-005 (manifests are ≤4 MB, so redirect saved negligible bandwidth).

Non-goals: no change to blob delivery (blobs still redirect); no per-call content-type override added to the storage interface; no ADR-005 edit (handbook-side, tracked separately). CDN offload for manifests is intentionally given up; monitor manifest-GET latency and storage Reader rate post-deploy.

Merge request reports

Loading
Loading