download/oci: Set Accept headers (#8720)

The ociTarget.Resolve and ociTarget.Fetch methods introduced in the
containerd migration were not setting Accept headers on manifest
requests. Several registries, including ghcr.io, require a manifest
media type in the Accept header and return 404 Not Found without it.

The previous containerd-based implementation set this header
automatically via the docker resolver. The new oras-based custom target
did not replicate that behaviour.

Signed-off-by: Charlie Egan <charlie_egan@apple.com>
This commit is contained in:
Charlie Egan
2026-06-02 10:20:37 +01:00
committed by GitHub
parent d474d70750
commit 8ee9808acc
2 changed files with 87 additions and 0 deletions
+16
View File
@@ -371,12 +371,23 @@ func (t *ociTarget) url(path string) string {
return fmt.Sprintf("%s://%s/v2/%s/%s", scheme, t.registry, t.repo, path)
}
// manifestMediaTypes lists the OCI and Docker manifest media types that registries
// such as ghcr.io require in the Accept header to return manifests correctly.
var manifestMediaTypes = strings.Join([]string{
"application/vnd.oci.image.manifest.v1+json",
"application/vnd.oci.image.index.v1+json",
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
"*/*",
}, ", ")
func (t *ociTarget) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
url := t.url("manifests/" + reference)
req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
if err != nil {
return ocispec.Descriptor{}, err
}
req.Header.Set("Accept", manifestMediaTypes)
resp, err := t.client.Do(req)
if err != nil {
@@ -408,12 +419,14 @@ func (t *ociTarget) Resolve(ctx context.Context, reference string) (ocispec.Desc
func (t *ociTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
// Use blobs endpoint for non-manifest content, manifests endpoint for manifests
var url string
isManifest := false
switch target.MediaType {
case "application/vnd.oci.image.manifest.v1+json",
"application/vnd.oci.image.index.v1+json",
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json":
url = t.url("manifests/" + target.Digest.String())
isManifest = true
default:
url = t.url("blobs/" + target.Digest.String())
}
@@ -422,6 +435,9 @@ func (t *ociTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.Re
if err != nil {
return nil, err
}
if isManifest {
req.Header.Set("Accept", target.MediaType)
}
resp, err := t.client.Do(req)
if err != nil {
+71
View File
@@ -0,0 +1,71 @@
//go:build !opa_no_oci
package download
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/registry/remote/auth"
)
// TestOCITargetAcceptHeaders verifies that ociTarget sends Accept headers on
// manifest requests. Registries such as ghcr.io return 404 for HEAD/GET requests
// that omit the Accept header; this test enforces the same behaviour.
func TestOCITargetAcceptHeaders(t *testing.T) {
const mediaType = "application/vnd.oci.image.manifest.v1+json"
digestHex := "sha256:" + strings.Repeat("a", 64)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// reject any request missing Accept entirely
if r.Header.Get("Accept") == "" {
w.WriteHeader(http.StatusNotFound)
return
}
// for GET (Fetch), Accept must be the exact descriptor media type
if r.Method == http.MethodGet && r.Header.Get("Accept") != mediaType {
w.WriteHeader(http.StatusNotAcceptable)
return
}
w.Header().Set("Content-Type", mediaType)
w.Header().Set("Docker-Content-Digest", digestHex)
w.Header().Set("Content-Length", "100")
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
serverURL, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
target := &ociTarget{
client: &auth.Client{Client: http.DefaultClient, Cache: auth.NewCache()},
registry: serverURL.Host,
repo: "org/repo",
plainHTTP: true,
}
// Resolve uses HEAD — any non-empty Accept is sufficient
if _, err := target.Resolve(context.Background(), "latest"); err != nil {
t.Fatalf("Resolve failed (Accept header likely missing): %v", err)
}
// Fetch uses GET — Accept must match the descriptor's media type exactly
desc := ocispec.Descriptor{
MediaType: mediaType,
Digest: digest.Digest(digestHex),
Size: 100,
}
rc, err := target.Fetch(context.Background(), desc)
if err != nil {
t.Fatalf("Fetch failed (Accept header likely missing or wrong): %v", err)
}
rc.Close()
}