Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions registry/remote/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,18 @@ func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, fn
ctx = withScopeHint(ctx, ref, auth.ActionPull)
url := buildArtifactReferrerURL(r.PlainHTTP, ref)
var err error

var legacyAPI bool
url, err = r.referrers(ctx, desc, fn, url, legacyAPI)
// Fallback to legacy url
if errors.Is(err, errdef.ErrNotFound) {
url = buildArtifactReferrerURLLegacy(r.PlainHTTP, ref)
legacyAPI = true
err = nil
}

for err == nil {
url, err = r.referrers(ctx, desc, fn, url)
url, err = r.referrers(ctx, desc, fn, url, legacyAPI)
}
if err != errNoLink {
return err
Expand All @@ -359,7 +369,7 @@ func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, fn

// referrers returns a single page of the manifest descriptors directly
// referencing the given manifest descriptor with the next link.
func (r *Repository) referrers(ctx context.Context, desc ocispec.Descriptor, fn func(referrers []artifactspec.Descriptor) error, url string) (string, error) {
func (r *Repository) referrers(ctx context.Context, desc ocispec.Descriptor, fn func(referrers []artifactspec.Descriptor) error, url string, legacyAPI bool) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
Expand All @@ -376,18 +386,28 @@ func (r *Repository) referrers(ctx context.Context, desc ocispec.Descriptor, fn
}
defer resp.Body.Close()

if resp.StatusCode == http.StatusNotFound {
return "", errdef.ErrNotFound
Comment thread
m5i-work marked this conversation as resolved.
Outdated
}
if resp.StatusCode != http.StatusOK {
return "", errutil.ParseErrorResponse(resp)
}

var page struct {
References []artifactspec.Descriptor `json:"references"`
Referrers []artifactspec.Descriptor `json:"referrers"`
}
lr := limitReader(resp.Body, r.MaxMetadataBytes)
if err := json.NewDecoder(lr).Decode(&page); err != nil {
return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
}
if err := fn(page.References); err != nil {
var refs []artifactspec.Descriptor
if legacyAPI {
refs = page.References
} else {
refs = page.Referrers
}
if err := fn(refs); err != nil {
return "", err
}

Expand Down
127 changes: 123 additions & 4 deletions registry/remote/repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -854,7 +854,7 @@ func TestRepository_Predecessors(t *testing.T) {
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/oras/artifacts/v1/test/manifests/" + manifestDesc.Digest.String() + "/referrers"
path := "/v2/test/_oras/artifacts/referrers"
if r.Method != http.MethodGet || r.URL.Path != path {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
Expand All @@ -875,11 +875,16 @@ func TestRepository_Predecessors(t *testing.T) {
case "bar":
referrers = referrerSet[2]
default:
if q.Get("digest") != manifestDesc.Digest.String() {
t.Errorf("digest not provided or mismatch: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := struct {
References []artifactspec.Descriptor `json:"references"`
References []artifactspec.Descriptor `json:"referrers"`
Comment thread
m5i-work marked this conversation as resolved.
Outdated
}{
References: referrers,
}
Expand Down Expand Up @@ -917,6 +922,121 @@ func TestRepository_Predecessors(t *testing.T) {
}

func TestRepository_Referrers(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest.FromBytes(manifest),
Size: int64(len(manifest)),
}
referrerSet := [][]artifactspec.Descriptor{
{
{
MediaType: artifactspec.MediaTypeArtifactManifest,
Size: 1,
Digest: digest.FromString("1"),
ArtifactType: "application/vnd.test",
},
{
MediaType: artifactspec.MediaTypeArtifactManifest,
Size: 2,
Digest: digest.FromString("2"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: artifactspec.MediaTypeArtifactManifest,
Size: 3,
Digest: digest.FromString("3"),
ArtifactType: "application/vnd.test",
},
{
MediaType: artifactspec.MediaTypeArtifactManifest,
Size: 4,
Digest: digest.FromString("4"),
ArtifactType: "application/vnd.test",
},
},
{
{
MediaType: artifactspec.MediaTypeArtifactManifest,
Size: 5,
Digest: digest.FromString("5"),
ArtifactType: "application/vnd.test",
},
},
}
var ts *httptest.Server
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/v2/test/_oras/artifacts/referrers"
if r.Method != http.MethodGet || r.URL.Path != path {
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
q := r.URL.Query()
n, err := strconv.Atoi(q.Get("n"))
if err != nil || n != 2 {
t.Errorf("bad page size: %s", q.Get("n"))
w.WriteHeader(http.StatusBadRequest)
return
}
var referrers []artifactspec.Descriptor
switch q.Get("test") {
case "foo":
referrers = referrerSet[1]
w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path))
case "bar":
referrers = referrerSet[2]
default:
if q.Get("digest") != manifestDesc.Digest.String() {
t.Errorf("digest not provided or mismatch: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
referrers = referrerSet[0]
w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path))
}
result := struct {
References []artifactspec.Descriptor `json:"referrers"`
}{
References: referrers,
}
if err := json.NewEncoder(w).Encode(result); err != nil {
t.Errorf("failed to write response: %v", err)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}

repo, err := NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
repo.ReferrerListPageSize = 2

ctx := context.Background()
index := 0
if err := repo.Referrers(ctx, manifestDesc, func(got []artifactspec.Descriptor) error {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
index++
if !reflect.DeepEqual(got, referrers) {
t.Errorf("Repository.Referrers() = %v, want %v", got, referrers)
}
return nil
}); err != nil {
t.Errorf("Repository.Referrers() error = %v", err)
}
}

func TestRepository_Referrers_Fallback(t *testing.T) {
manifest := []byte(`{"layers":[]}`)
manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Expand Down Expand Up @@ -965,7 +1085,6 @@ func TestRepository_Referrers(t *testing.T) {
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := "/oras/artifacts/v1/test/manifests/" + manifestDesc.Digest.String() + "/referrers"
if r.Method != http.MethodGet || r.URL.Path != path {
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
return
}
Expand Down Expand Up @@ -1012,7 +1131,7 @@ func TestRepository_Referrers(t *testing.T) {
ctx := context.Background()
index := 0
if err := repo.Referrers(ctx, manifestDesc, func(got []artifactspec.Descriptor) error {
if index > 2 {
if index >= len(referrerSet) {
t.Fatalf("out of index bound: %d", index)
}
referrers := referrerSet[index]
Expand Down
18 changes: 14 additions & 4 deletions registry/remote/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,10 @@ func buildRepositoryBlobUploadURL(plainHTTP bool, ref registry.Reference) string
return buildRepositoryBaseURL(plainHTTP, ref) + "/blobs/uploads/"
}

// buildArtifactReferrerURL builds the URL for accessing the manifest referrers
// API.
// buildArtifactReferrerURLLegacy builds the URL for accessing the manifest referrers API in artifact spec v1.0.0-draft.1.
// Format: <scheme>://<registry>/oras/artifacts/v1/<repository>/manifests/<digest>/referrers
// Reference: https://github.com/oras-project/artifacts-spec/blob/main/manifest-referrers-api.md
func buildArtifactReferrerURL(plainHTTP bool, ref registry.Reference) string {
// Reference: https://github.com/oras-project/artifacts-spec/blob/v1.0.0-draft.1/manifest-referrers-api.md
func buildArtifactReferrerURLLegacy(plainHTTP bool, ref registry.Reference) string {
return fmt.Sprintf(
"%s://%s/oras/artifacts/v1/%s/manifests/%s/referrers",
buildScheme(plainHTTP),
Expand All @@ -99,3 +98,14 @@ func buildArtifactReferrerURL(plainHTTP bool, ref registry.Reference) string {
ref.Reference,
)
}

// buildArtifactReferrerURL builds the URL for accessing the manifest referrers API in artifact spec v1.0.0-rc.1.
// Format: <scheme>://<registry>/v2/<repository>/_oras/artifacts/referrers?digest=<digest>
// Reference: https://github.com/oras-project/artifacts-spec/blob/v1.0.0-rc.1/manifest-referrers-api.md
func buildArtifactReferrerURL(plainHTTP bool, ref registry.Reference) string {
Comment thread
m5i-work marked this conversation as resolved.
return fmt.Sprintf(
"%s/_oras/artifacts/referrers?digest=%s",
buildRepositoryBaseURL(plainHTTP, ref),
ref.Reference,
)
}