diff --git a/extendedcopy.go b/extendedcopy.go index 72982fad..e4e03c3d 100644 --- a/extendedcopy.go +++ b/extendedcopy.go @@ -22,7 +22,6 @@ import ( "regexp" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/internal/copyutil" "oras.land/oras-go/v2/internal/descriptor" @@ -182,11 +181,33 @@ func findRoots(ctx context.Context, storage content.ReadOnlyGraphStorage, node o // For performance consideration, when using both FilterArtifactType and // FilterAnnotation, it's recommended to call FilterArtifactType first. func (opts *ExtendedCopyGraphOptions) FilterAnnotation(key string, regex *regexp.Regexp) { + filter := func(filtered []ocispec.Descriptor, predecessors []ocispec.Descriptor) []ocispec.Descriptor { + for _, p := range predecessors { + if value, ok := p.Annotations[key]; ok && (regex == nil || regex.MatchString(value)) { + filtered = append(filtered, p) + } + } + return filtered + } + fp := opts.FindPredecessors opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { var predecessors []ocispec.Descriptor var err error + + referrerFinder, supportReferrers := src.(registry.ReferrerFinder) if fp == nil { + if supportReferrers { + // if src is a ReferrerFinder, use Referrers() for possible memory saving + if err := referrerFinder.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { + // for each page of the results, filter the referrers + predecessors = filter(predecessors, referrers) + return nil + }); err != nil { + return nil, err + } + return predecessors, nil + } predecessors, err = src.Predecessors(ctx, desc) } else { predecessors, err = fp(ctx, src, desc) @@ -194,40 +215,30 @@ func (opts *ExtendedCopyGraphOptions) FilterAnnotation(key string, regex *regexp if err != nil { return nil, err } - var filtered []ocispec.Descriptor - for _, p := range predecessors { - if p.Annotations == nil { - switch p.MediaType { - case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest, - docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex, - artifactspec.MediaTypeArtifactManifest: - if err = func() error { - rc, err := src.Fetch(ctx, p) + + if !supportReferrers { + // For src that does not support Referrers, Predecessors is used. + // However, the descriptors returned by Predecessors may not include + // annotations of the corresponding manifests. + for i, p := range predecessors { + if p.Annotations == nil { + // if the annotations are not present in the descriptors, + // fetch it from the manifest content. + switch p.MediaType { + case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest, + docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex, + ocispec.MediaTypeArtifactManifest: + annotations, err := fetchAnnotations(ctx, src, p) if err != nil { - return err - } - defer rc.Close() - var manifest struct { - Annotations map[string]string `json:"annotations"` - } - if err := json.NewDecoder(rc).Decode(&manifest); err != nil { - return err - } - if manifest.Annotations == nil { - p.Annotations = map[string]string{} - } else { - p.Annotations = manifest.Annotations + return nil, err } - return nil - }(); err != nil { - return nil, err + predecessors[i].Annotations = annotations } } } - if value, ok := p.Annotations[key]; ok && (regex == nil || regex.MatchString(value)) { - filtered = append(filtered, p) - } } + var filtered []ocispec.Descriptor + filtered = filter(filtered, predecessors) return filtered, nil } } @@ -241,15 +252,33 @@ func (opts *ExtendedCopyGraphOptions) FilterArtifactType(regex *regexp.Regexp) { if regex == nil { return } + filter := func(filtered []ocispec.Descriptor, predecessors []ocispec.Descriptor) []ocispec.Descriptor { + for _, p := range predecessors { + if regex.MatchString(p.ArtifactType) { + filtered = append(filtered, p) + } + } + return filtered + } + fp := opts.FindPredecessors opts.FindPredecessors = func(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { var predecessors []ocispec.Descriptor var err error + referrerFinder, supportReferrers := src.(registry.ReferrerFinder) if fp == nil { - // if src is a ReferrerFinder, use Referrers() to filter the predecessors. - if rf, ok := src.(registry.ReferrerFinder); ok { - return findReferrersAndFilter(rf, ctx, desc, regex) + if supportReferrers { + // if src is a ReferrerFinder, use Referrers() for possible memory saving + if err := referrerFinder.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { + // for each page of the results, filter the referrers + predecessors = filter(predecessors, referrers) + return nil + }); err != nil { + return nil, err + } + return predecessors, nil } + predecessors, err = src.Predecessors(ctx, desc) } else { predecessors, err = fp(ctx, src, desc) @@ -257,46 +286,56 @@ func (opts *ExtendedCopyGraphOptions) FilterArtifactType(regex *regexp.Regexp) { if err != nil { return nil, err } - var filtered []ocispec.Descriptor - // for each predecessor, decode the manifest and check its artifact type. - for _, p := range predecessors { - if p.MediaType == artifactspec.MediaTypeArtifactManifest { - if err = func() error { - rc, err := src.Fetch(ctx, p) + + if !supportReferrers { + // For src that does not support Referrers, Predecessors is used. + // However, the descriptors returned by Predecessors may not include + // the artifact type of the corresponding manifests. + for i, p := range predecessors { + if p.MediaType == ocispec.MediaTypeArtifactManifest && p.ArtifactType == "" { + // if the artifact type is not present in the descriptors, + // fetch it from the manifest content. + artifactType, err := fetchArtifactType(ctx, src, p) if err != nil { - return err - } - defer rc.Close() - var manifest artifactspec.Manifest - if err := json.NewDecoder(rc).Decode(&manifest); err != nil { - return err - } - if regex.MatchString(manifest.ArtifactType) { - filtered = append(filtered, p) + return nil, err } - return nil - }(); err != nil { - return nil, err + predecessors[i].ArtifactType = artifactType } } } + + var filtered []ocispec.Descriptor + filtered = filter(filtered, predecessors) return filtered, nil } } -// findReferrersAndFilter filters the predecessors with Referrers. -func findReferrersAndFilter(rf registry.ReferrerFinder, ctx context.Context, desc ocispec.Descriptor, regex *regexp.Regexp) ([]ocispec.Descriptor, error) { - var predecessors []ocispec.Descriptor - if err := rf.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { - // for each page of the results, do the following: - for _, referrer := range referrers { - if regex.MatchString(referrer.ArtifactType) { - predecessors = append(predecessors, referrer) - } - } - return nil - }); err != nil { +// fetchAnnotations fetches the annotations of the manifest described by desc. +func fetchAnnotations(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (map[string]string, error) { + rc, err := src.Fetch(ctx, desc) + if err != nil { return nil, err } - return predecessors, nil + defer rc.Close() + var manifest struct { + Annotations map[string]string `json:"annotations"` + } + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + return nil, err + } + return manifest.Annotations, nil +} + +// fetchArtifactType fetches the artifact type of the manifest described by desc. +func fetchArtifactType(ctx context.Context, src content.ReadOnlyGraphStorage, desc ocispec.Descriptor) (string, error) { + rc, err := src.Fetch(ctx, desc) + if err != nil { + return "", err + } + defer rc.Close() + var manifest ocispec.Artifact + if err := json.NewDecoder(rc).Decode(&manifest); err != nil { + return "", err + } + return manifest.ArtifactType, nil } diff --git a/extendedcopy_test.go b/extendedcopy_test.go index 554702ac..a28df1de 100644 --- a/extendedcopy_test.go +++ b/extendedcopy_test.go @@ -31,13 +31,11 @@ import ( "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - artifactspec "github.com/oras-project/artifacts-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/errdef" - "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/registry/remote" ) @@ -68,9 +66,11 @@ func TestExtendedCopy_FullCopy(t *testing.T) { appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) } generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { - var manifest ocispec.Artifact - manifest.Subject = &subject - manifest.Blobs = append(manifest.Blobs, blobs...) + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Blobs: blobs, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) @@ -166,9 +166,11 @@ func TestExtendedCopyGraph_FullCopy(t *testing.T) { appendBlob(ocispec.MediaTypeImageIndex, indexJSON) } generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { - var manifest ocispec.Artifact - manifest.Subject = &subject - manifest.Blobs = append(manifest.Blobs, blobs...) + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Blobs: blobs, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) @@ -371,9 +373,11 @@ func TestExtendedCopyGraph_WithDepthOption(t *testing.T) { appendBlob(ocispec.MediaTypeImageIndex, indexJSON) } generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { - var manifest ocispec.Artifact - manifest.Subject = &subject - manifest.Blobs = append(manifest.Blobs, blobs...) + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Blobs: blobs, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) @@ -503,9 +507,11 @@ func TestExtendedCopyGraph_WithFindPredecessorsOption(t *testing.T) { appendBlob(ocispec.MediaTypeImageIndex, indexJSON) } generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { - var manifest ocispec.Artifact - manifest.Subject = &subject - manifest.Blobs = append(manifest.Blobs, blobs...) + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Blobs: blobs, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) @@ -598,32 +604,32 @@ func TestExtendedCopyGraph_FilterAnnotationWithRegex(t *testing.T) { // generate test content var blobs [][]byte var descs []ocispec.Descriptor - appendBlob := func(mediaType string, blob []byte, key string, value string) { + appendBlob := func(mediaType string, blob []byte) { blobs = append(blobs, blob) descs = append(descs, ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), - Annotations: map[string]string{key: value}, + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), }) } generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.Annotations = map[string]string{key: value} + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Annotations: map[string]string{key: value}, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON, key, value) + appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON) } - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo"), "bar", "blackpink") // descs[0] - generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] - generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] - generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] - generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] - generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] + generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] + generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] + generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] + generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] ctx := context.Background() verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { for _, i := range copiedIndice { @@ -694,33 +700,33 @@ func TestExtendedCopyGraph_FilterAnnotationWithMultipleRegex(t *testing.T) { // generate test content var blobs [][]byte var descs []ocispec.Descriptor - appendBlob := func(mediaType string, blob []byte, key string, value string) { + appendBlob := func(mediaType string, blob []byte) { blobs = append(blobs, blob) descs = append(descs, ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), - Annotations: map[string]string{key: value}, + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), }) } generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.Annotations = map[string]string{key: value} + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Annotations: map[string]string{key: value}, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON, key, value) + appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON) } - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo"), "bar", "blackpink") // descs[0] - generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] - generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] - generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] - generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] - generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] - generateArtifactManifest(descs[0], "bar", "blackblack") // descs[6] + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] + generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] + generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] + generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] + generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] + generateArtifactManifest(descs[0], "bar", "blackblack") // descs[6] ctx := context.Background() verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { for _, i := range copiedIndice { @@ -796,35 +802,37 @@ func TestExtendedCopyGraph_FilterAnnotationWithMultipleRegex(t *testing.T) { verifyCopy(dst, copiedIndice, uncopiedIndice) } -func TestExtendedCopyGraph_FilterAnnotationWithRegexNoAnnotationInDescriptor(t *testing.T) { +func TestExtendedCopyGraph_FilterAnnotationWithRegex_AnnotationInDescriptor(t *testing.T) { // generate test content var blobs [][]byte var descs []ocispec.Descriptor - appendBlob := func(mediaType string, blob []byte) { + appendBlob := func(mediaType, key, value string, blob []byte) { blobs = append(blobs, blob) descs = append(descs, ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + Annotations: map[string]string{key: value}, }) } generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.Annotations = map[string]string{key: value} + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Annotations: map[string]string{key: value}, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON) + appendBlob(ocispec.MediaTypeArtifactManifest, key, value, manifestJSON) } - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0] - generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] - generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] - generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] - generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] - generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] + appendBlob(ocispec.MediaTypeImageLayer, "", "", []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] + generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] + generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] + generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] + generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] ctx := context.Background() verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { for _, i := range copiedIndice { @@ -864,6 +872,174 @@ func TestExtendedCopyGraph_FilterAnnotationWithRegexNoAnnotationInDescriptor(t * verifyCopy(dst, copiedIndice, uncopiedIndice) } +func TestExtendedCopyGraph_FilterAnnotationWithMultipleRegex_Referrers(t *testing.T) { + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType, key, value string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + Annotations: map[string]string{key: value}, + }) + } + generateArtifactManifest := func(subject ocispec.Descriptor, key string, value string) { + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + Subject: &subject, + Annotations: map[string]string{key: value}, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeArtifactManifest, key, value, manifestJSON) + } + appendBlob(ocispec.MediaTypeImageLayer, "", "", []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "bar", "bluebrown") // descs[1] + generateArtifactManifest(descs[0], "bar", "blackred") // descs[2] + generateArtifactManifest(descs[0], "bar", "blackviolet") // descs[3] + generateArtifactManifest(descs[0], "bar", "greengrey") // descs[4] + generateArtifactManifest(descs[0], "bar", "brownblack") // descs[5] + generateArtifactManifest(descs[0], "bar", "blackblack") // descs[6] + + // set up test server + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path + w.Header().Set("ORAS-Api-Version", "oras/1.0") + switch { + case strings.Contains(p, descs[0].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer) + w.Header().Set("Content-Digest", descs[0].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[0]))) + w.Write(blobs[0]) + case strings.Contains(p, descs[1].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[1].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[1]))) + w.Write(blobs[1]) + case strings.Contains(p, descs[2].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[2].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[2]))) + w.Write(blobs[2]) + case strings.Contains(p, descs[3].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[3].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[3]))) + w.Write(blobs[3]) + case strings.Contains(p, descs[4].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[4].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[4]))) + w.Write(blobs[4]) + case strings.Contains(p, descs[5].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[5].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[5]))) + w.Write(blobs[5]) + case strings.Contains(p, "referrers"): + q := r.URL.Query() + var referrers []ocispec.Descriptor + if q.Get("digest") == descs[0].Digest.String() { + referrers = descs[1:] + } + result := struct { + Referrers []ocispec.Descriptor `json:"referrers"` + }{ + Referrers: referrers, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Errorf("failed to write response: %v", err) + } + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + return + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Errorf("invalid test http server: %v", err) + } + + ctx := context.Background() + verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { + for _, i := range copiedIndice { + got, err := content.FetchAll(ctx, dst, descs[i]) + if err != nil { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, false) + continue + } + if want := blobs[i]; !bytes.Equal(got, want) { + t.Errorf("content[%d] = %v, want %v", i, got, want) + } + } + for _, i := range uncopiedIndice { + if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound) + } + } + } + + src, err := remote.NewRepository(uri.Host + "/test") + if err != nil { + t.Errorf("NewRepository() error = %v", err) + } + + // test extended copy by descs[0] with two annotation filters + dst := memory.New() + opts := oras.ExtendedCopyGraphOptions{} + exp1 := "black." + exp2 := ".pink|red" + regex1 := regexp.MustCompile(exp1) + regex2 := regexp.MustCompile(exp2) + opts.FilterAnnotation("bar", regex1) + opts.FilterAnnotation("bar", regex2) + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + copiedIndice := []int{0, 2} + uncopiedIndice := []int{1, 3, 4, 5, 6} + verifyCopy(dst, copiedIndice, uncopiedIndice) + + // test extended copy by descs[0] with three annotation filters, nil included + dst = memory.New() + opts = oras.ExtendedCopyGraphOptions{} + exp1 = "black." + exp2 = ".pink|red" + regex1 = regexp.MustCompile(exp1) + regex2 = regexp.MustCompile(exp2) + opts.FilterAnnotation("bar", regex1) + opts.FilterAnnotation("bar", nil) + opts.FilterAnnotation("bar", regex2) + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + copiedIndice = []int{0, 2} + uncopiedIndice = []int{1, 3, 4, 5, 6} + verifyCopy(dst, copiedIndice, uncopiedIndice) + + // test extended copy by descs[0] with two annotation filters, the second filter has an unavailable key + dst = memory.New() + opts = oras.ExtendedCopyGraphOptions{} + exp1 = "black." + exp2 = ".pink|red" + regex1 = regexp.MustCompile(exp1) + regex2 = regexp.MustCompile(exp2) + opts.FilterAnnotation("bar", regex1) + opts.FilterAnnotation("test", regex2) + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil { + t.Fatalf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + copiedIndice = []int{0} + uncopiedIndice = []int{1, 2, 3, 4, 5, 6} + verifyCopy(dst, copiedIndice, uncopiedIndice) +} + func TestExtendedCopyGraph_FilterArtifactTypeWithRegex(t *testing.T) { // generate test content var blobs [][]byte @@ -877,23 +1053,24 @@ func TestExtendedCopyGraph_FilterArtifactTypeWithRegex(t *testing.T) { }) } generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.ArtifactType = artifactType + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Subject: &subject, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON) + appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON) } - appendBlob(ocispec.MediaTypeImageConfig, []byte("foo")) // descs[0] - generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] - generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] - generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] - generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] - generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] + generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] + generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] + generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] + generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] ctx := context.Background() verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { @@ -958,22 +1135,23 @@ func TestExtendedCopyGraph_FilterArtifactTypeWithMultipleRegex(t *testing.T) { }) } generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.ArtifactType = artifactType + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Subject: &subject, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON) + appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON) } - appendBlob(ocispec.MediaTypeImageConfig, []byte("foo")) // descs[0] - generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] - generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] - generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] - generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] - generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] + generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] + generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] + generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] + generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] ctx := context.Background() verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { @@ -1038,50 +1216,121 @@ func TestExtendedCopyGraph_FilterArtifactTypeWithMultipleRegex(t *testing.T) { verifyCopy(dst, copiedIndice, uncopiedIndice) } -func TestExtendedCopyGraph_FilterArtifactTypeByReferrersWithMultipleRegex(t *testing.T) { +func TestExtendedCopyGraph_FilterArtifactTypeWithRegex_ArtifactTypeInDescriptor(t *testing.T) { // generate test content var blobs [][]byte var descs []ocispec.Descriptor - var referrerSet []ocispec.Descriptor - appendBlob := func(mediaType string, blob []byte) { + appendBlob := func(mediaType string, artifactType string, blob []byte) { blobs = append(blobs, blob) descs = append(descs, ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), + MediaType: mediaType, + ArtifactType: artifactType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), }) } generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.ArtifactType = artifactType + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Subject: &subject, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON) + appendBlob(ocispec.MediaTypeArtifactManifest, artifactType, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageLayer, "", []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] + generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] + generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] + generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] + generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] + + ctx := context.Background() + verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { + for _, i := range copiedIndice { + got, err := content.FetchAll(ctx, dst, descs[i]) + if err != nil { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, false) + continue + } + if want := blobs[i]; !bytes.Equal(got, want) { + t.Errorf("content[%d] = %v, want %v", i, got, want) + } + } + for _, i := range uncopiedIndice { + if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound) + } + } + } + + src := memory.New() + for i := range blobs { + err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Errorf("failed to push test content to src: %d: %v", i, err) + } + } + + // test extended copy by descs[0], include the predecessors whose artifact + // type matches exp. + exp := ".bar." + dst := memory.New() + opts := oras.ExtendedCopyGraphOptions{} + regex := regexp.MustCompile(exp) + opts.FilterArtifactType(regex) + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil { + t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + copiedIndice := []int{0, 1, 3, 4} + uncopiedIndice := []int{2, 5} + verifyCopy(dst, copiedIndice, uncopiedIndice) + + // test extended copy by descs[0] with no regex + // type matches exp. + opts = oras.ExtendedCopyGraphOptions{} + opts.FilterArtifactType(nil) + if opts.FindPredecessors != nil { + t.Fatal("FindPredecessors not nil!") } - pushReferrers := func(desc ocispec.Descriptor, artifactType string) { - referrerSet = append(referrerSet, ocispec.Descriptor{ - MediaType: desc.MediaType, +} + +func TestExtendedCopyGraph_FilterArtifactTypeWithMultipleRegex_Referrers(t *testing.T) { + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, artifactType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, ArtifactType: artifactType, - Digest: desc.Digest, - Size: desc.Size, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), }) } + generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string) { + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Subject: &subject, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeArtifactManifest, artifactType, manifestJSON) + } - appendBlob(ocispec.MediaTypeImageConfig, []byte("foo")) // descs[0] - generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] - generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] - generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] - generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] - generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] - pushReferrers(descs[1], "good-bar-yellow") - pushReferrers(descs[2], "bad-woo-red") - pushReferrers(descs[3], "bad-bar-blue") - pushReferrers(descs[4], "bad-bar-red") - pushReferrers(descs[5], "good-woo-pink") + appendBlob(ocispec.MediaTypeImageLayer, "", []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "good-bar-yellow") // descs[1] + generateArtifactManifest(descs[0], "bad-woo-red") // descs[2] + generateArtifactManifest(descs[0], "bad-bar-blue") // descs[3] + generateArtifactManifest(descs[0], "bad-bar-red") // descs[4] + generateArtifactManifest(descs[0], "good-woo-pink") // descs[5] // set up test server ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1089,32 +1338,32 @@ func TestExtendedCopyGraph_FilterArtifactTypeByReferrersWithMultipleRegex(t *tes w.Header().Set("ORAS-Api-Version", "oras/1.0") switch { case strings.Contains(p, descs[0].Digest.String()): - w.Header().Set("Content-Type", ocispec.MediaTypeImageConfig) + w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer) w.Header().Set("Content-Digest", descs[0].Digest.String()) w.Header().Set("Content-Length", strconv.Itoa(len(blobs[0]))) w.Write(blobs[0]) case strings.Contains(p, descs[1].Digest.String()): - w.Header().Set("Content-Type", artifactspec.MediaTypeArtifactManifest) + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) w.Header().Set("Content-Digest", descs[1].Digest.String()) w.Header().Set("Content-Length", strconv.Itoa(len(blobs[1]))) w.Write(blobs[1]) case strings.Contains(p, descs[2].Digest.String()): - w.Header().Set("Content-Type", artifactspec.MediaTypeArtifactManifest) + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) w.Header().Set("Content-Digest", descs[2].Digest.String()) w.Header().Set("Content-Length", strconv.Itoa(len(blobs[2]))) w.Write(blobs[2]) case strings.Contains(p, descs[3].Digest.String()): - w.Header().Set("Content-Type", artifactspec.MediaTypeArtifactManifest) + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) w.Header().Set("Content-Digest", descs[3].Digest.String()) w.Header().Set("Content-Length", strconv.Itoa(len(blobs[3]))) w.Write(blobs[3]) case strings.Contains(p, descs[4].Digest.String()): - w.Header().Set("Content-Type", artifactspec.MediaTypeArtifactManifest) + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) w.Header().Set("Content-Digest", descs[4].Digest.String()) w.Header().Set("Content-Length", strconv.Itoa(len(blobs[4]))) w.Write(blobs[4]) case strings.Contains(p, descs[5].Digest.String()): - w.Header().Set("Content-Type", artifactspec.MediaTypeArtifactManifest) + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) w.Header().Set("Content-Digest", descs[5].Digest.String()) w.Header().Set("Content-Length", strconv.Itoa(len(blobs[5]))) w.Write(blobs[5]) @@ -1122,7 +1371,7 @@ func TestExtendedCopyGraph_FilterArtifactTypeByReferrersWithMultipleRegex(t *tes q := r.URL.Query() var referrers []ocispec.Descriptor if q.Get("digest") == descs[0].Digest.String() { - referrers = referrerSet + referrers = descs[1:] } result := struct { Referrers []ocispec.Descriptor `json:"referrers"` @@ -1190,37 +1439,37 @@ func TestExtendedCopyGraph_FilterArtifactTypeAndAnnotationWithMultipleRegex(t *t // generate test content var blobs [][]byte var descs []ocispec.Descriptor - appendBlob := func(mediaType string, blob []byte, value string) { + appendBlob := func(mediaType string, blob []byte) { blobs = append(blobs, blob) descs = append(descs, ocispec.Descriptor{ - MediaType: mediaType, - Digest: digest.FromBytes(blob), - Size: int64(len(blob)), - Annotations: map[string]string{"rank": value}, + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), }) } generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string, value string) { - var manifest artifactspec.Manifest - artifactSubject := descriptor.OCIToArtifact(subject) - manifest.Subject = &artifactSubject - manifest.ArtifactType = artifactType - manifest.Annotations = map[string]string{"rank": value} + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Subject: &subject, + Annotations: map[string]string{"rank": value}, + } manifestJSON, err := json.Marshal(manifest) if err != nil { t.Fatal(err) } - appendBlob(artifactspec.MediaTypeArtifactManifest, manifestJSON, value) + appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON) } - appendBlob(ocispec.MediaTypeImageConfig, []byte("foo"), "na") // descs[0] - generateArtifactManifest(descs[0], "good-bar-yellow", "1st") // descs[1] - generateArtifactManifest(descs[0], "bad-woo-red", "1st") // descs[2] - generateArtifactManifest(descs[0], "bad-bar-blue", "2nd") // descs[3] - generateArtifactManifest(descs[0], "bad-bar-red", "3rd") // descs[4] - generateArtifactManifest(descs[0], "good-woo-pink", "2nd") // descs[5] - generateArtifactManifest(descs[0], "good-foo-blue", "3rd") // descs[6] - generateArtifactManifest(descs[0], "bad-bar-orange", "4th") // descs[7] - generateArtifactManifest(descs[0], "bad-woo-white", "4th") // descs[8] - generateArtifactManifest(descs[0], "good-woo-orange", "na") // descs[9] + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // descs[0] + generateArtifactManifest(descs[0], "good-bar-yellow", "1st") // descs[1] + generateArtifactManifest(descs[0], "bad-woo-red", "1st") // descs[2] + generateArtifactManifest(descs[0], "bad-bar-blue", "2nd") // descs[3] + generateArtifactManifest(descs[0], "bad-bar-red", "3rd") // descs[4] + generateArtifactManifest(descs[0], "good-woo-pink", "2nd") // descs[5] + generateArtifactManifest(descs[0], "good-foo-blue", "3rd") // descs[6] + generateArtifactManifest(descs[0], "bad-bar-orange", "4th") // descs[7] + generateArtifactManifest(descs[0], "bad-woo-white", "4th") // descs[8] + generateArtifactManifest(descs[0], "good-woo-orange", "na") // descs[9] ctx := context.Background() verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { @@ -1272,3 +1521,169 @@ func TestExtendedCopyGraph_FilterArtifactTypeAndAnnotationWithMultipleRegex(t *t uncopiedIndice := []int{1, 2, 4, 5, 6, 8, 9} verifyCopy(dst, copiedIndice, uncopiedIndice) } + +func TestExtendedCopyGraph_FilterArtifactTypeAndAnnotationWithMultipleRegex_Referrers(t *testing.T) { + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, artifactType string, blob []byte, value string) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + ArtifactType: artifactType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + Annotations: map[string]string{"rank": value}, + }) + } + generateArtifactManifest := func(subject ocispec.Descriptor, artifactType string, value string) { + manifest := ocispec.Artifact{ + MediaType: ocispec.MediaTypeArtifactManifest, + ArtifactType: artifactType, + Subject: &subject, + Annotations: map[string]string{"rank": value}, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeArtifactManifest, artifactType, manifestJSON, value) + } + appendBlob(ocispec.MediaTypeImageLayer, "", []byte("foo"), "na") // descs[0] + generateArtifactManifest(descs[0], "good-bar-yellow", "1st") // descs[1] + generateArtifactManifest(descs[0], "bad-woo-red", "1st") // descs[2] + generateArtifactManifest(descs[0], "bad-bar-blue", "2nd") // descs[3] + generateArtifactManifest(descs[0], "bad-bar-red", "3rd") // descs[4] + generateArtifactManifest(descs[0], "good-woo-pink", "2nd") // descs[5] + generateArtifactManifest(descs[0], "good-foo-blue", "3rd") // descs[6] + generateArtifactManifest(descs[0], "bad-bar-orange", "4th") // descs[7] + generateArtifactManifest(descs[0], "bad-woo-white", "4th") // descs[8] + generateArtifactManifest(descs[0], "good-woo-orange", "na") // descs[9] + + // set up test server + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path + w.Header().Set("ORAS-Api-Version", "oras/1.0") + switch { + case strings.Contains(p, descs[0].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeImageLayer) + w.Header().Set("Content-Digest", descs[0].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[0]))) + w.Write(blobs[0]) + case strings.Contains(p, descs[1].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[1].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[1]))) + w.Write(blobs[1]) + case strings.Contains(p, descs[2].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[2].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[2]))) + w.Write(blobs[2]) + case strings.Contains(p, descs[3].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[3].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[3]))) + w.Write(blobs[3]) + case strings.Contains(p, descs[4].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[4].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[4]))) + w.Write(blobs[4]) + case strings.Contains(p, descs[5].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[5].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[5]))) + w.Write(blobs[5]) + case strings.Contains(p, descs[6].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[6].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[6]))) + w.Write(blobs[6]) + case strings.Contains(p, descs[7].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[7].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[7]))) + w.Write(blobs[7]) + case strings.Contains(p, descs[8].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[8].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[8]))) + w.Write(blobs[8]) + case strings.Contains(p, descs[9].Digest.String()): + w.Header().Set("Content-Type", ocispec.MediaTypeArtifactManifest) + w.Header().Set("Content-Digest", descs[9].Digest.String()) + w.Header().Set("Content-Length", strconv.Itoa(len(blobs[9]))) + w.Write(blobs[9]) + case strings.Contains(p, "referrers"): + q := r.URL.Query() + var referrers []ocispec.Descriptor + if q.Get("digest") == descs[0].Digest.String() { + referrers = descs[1:] + } + result := struct { + Referrers []ocispec.Descriptor `json:"referrers"` + }{ + Referrers: referrers, + } + if err := json.NewEncoder(w).Encode(result); err != nil { + t.Errorf("failed to write response: %v", err) + } + default: + t.Errorf("unexpected access: %s %s", r.Method, r.URL) + w.WriteHeader(http.StatusNotFound) + return + } + })) + defer ts.Close() + uri, err := url.Parse(ts.URL) + if err != nil { + t.Errorf("invalid test http server: %v", err) + } + + ctx := context.Background() + verifyCopy := func(dst content.Fetcher, copiedIndice []int, uncopiedIndice []int) { + for _, i := range copiedIndice { + got, err := content.FetchAll(ctx, dst, descs[i]) + if err != nil { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, false) + continue + } + if want := blobs[i]; !bytes.Equal(got, want) { + t.Errorf("content[%d] = %v, want %v", i, got, want) + } + } + for _, i := range uncopiedIndice { + if _, err := content.FetchAll(ctx, dst, descs[i]); !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("content[%d] error = %v, wantErr %v", i, err, errdef.ErrNotFound) + } + } + } + + src, err := remote.NewRepository(uri.Host + "/test") + if err != nil { + t.Errorf("NewRepository() error = %v", err) + } + // test extended copy by descs[0], include the predecessors whose artifact + // type and annotation match the regular expressions. + typeExp1 := ".foo|bar." + typeExp2 := "bad." + annotationExp1 := "[1-4]." + annotationExp2 := "2|4." + dst := memory.New() + opts := oras.ExtendedCopyGraphOptions{} + typeRegex1 := regexp.MustCompile(typeExp1) + typeRegex2 := regexp.MustCompile(typeExp2) + annotationRegex1 := regexp.MustCompile(annotationExp1) + annotationRegex2 := regexp.MustCompile(annotationExp2) + opts.FilterAnnotation("rank", annotationRegex1) + opts.FilterArtifactType(typeRegex1) + opts.FilterAnnotation("rank", annotationRegex2) + opts.FilterArtifactType(typeRegex2) + if err := oras.ExtendedCopyGraph(ctx, src, dst, descs[0], opts); err != nil { + t.Errorf("ExtendedCopyGraph() error = %v, wantErr %v", err, false) + } + copiedIndice := []int{0, 3, 7} + uncopiedIndice := []int{1, 2, 4, 5, 6, 8, 9} + verifyCopy(dst, copiedIndice, uncopiedIndice) +}