mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
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:
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user