diff --git a/copy.go b/copy.go index 61989c3e..1a9f66a7 100644 --- a/copy.go +++ b/copy.go @@ -57,37 +57,36 @@ type CopyOptions struct { } // selectPlatform implements platform filter and returns the descriptor of -// the first matched manifest from the manifest list / image index -func (o *CopyOptions) selectPlatform(ctx context.Context, src content.Storage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) { - if root.MediaType == docker.MediaTypeManifestList || root.MediaType == ocispec.MediaTypeImageIndex { - manifests, err := content.Successors(ctx, src, root) - if err != nil { - return ocispec.Descriptor{}, err - } +// the first matched manifest from the manifest list / image index. +func selectPlatform(ctx context.Context, src content.Storage, root ocispec.Descriptor, p *ocispec.Platform) (ocispec.Descriptor, error) { + if root.MediaType != docker.MediaTypeManifestList && root.MediaType != ocispec.MediaTypeImageIndex { + return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported) + } - // platform filter - for _, m := range manifests { - matched := platform.MatchPlatform(m.Platform, p) - if matched { - return m, nil - } + manifests, err := content.Successors(ctx, src, root) + if err != nil { + return ocispec.Descriptor{}, err + } + + // platform filter + for _, m := range manifests { + if platform.MatchPlatform(m.Platform, p) { + return m, nil } - return ocispec.Descriptor{}, errdef.ErrNotFound } return ocispec.Descriptor{}, errdef.ErrNotFound } -// AddPlatformFilter adds the selectPlatform func into the MapRoot func -func (o *CopyOptions) AddPlatformFilter(p *ocispec.Platform) { +// WithPlatformFilter adds the check on the platform attributes. +func (o *CopyOptions) WithPlatformFilter(p *ocispec.Platform) { mapRoot := o.MapRoot - o.MapRoot = func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (ocispec.Descriptor, error) { - var err error + o.MapRoot = func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (desc ocispec.Descriptor, err error) { if mapRoot != nil { if root, err = mapRoot(ctx, src, root); err != nil { return ocispec.Descriptor{}, err } } - return o.selectPlatform(ctx, src, root, p) + return selectPlatform(ctx, src, root, p) } } diff --git a/copy_test.go b/copy_test.go index da37a18a..ede33050 100644 --- a/copy_test.go +++ b/copy_test.go @@ -477,7 +477,7 @@ func TestCopy_WithOptions(t *testing.T) { Size: int64(len(blob)), }) } - appendManifest := func(arc, os, variant string, mediaType string, blob []byte) { + appendManifest := func(arc, os string, mediaType string, blob []byte) { blobs = append(blobs, blob) descs = append(descs, ocispec.Descriptor{ MediaType: mediaType, @@ -486,11 +486,10 @@ func TestCopy_WithOptions(t *testing.T) { Platform: &ocispec.Platform{ Architecture: arc, OS: os, - Variant: variant, }, }) } - generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + generateManifest := func(arc, os string, config ocispec.Descriptor, layers ...ocispec.Descriptor) { manifest := ocispec.Manifest{ Config: config, Layers: layers, @@ -499,7 +498,7 @@ func TestCopy_WithOptions(t *testing.T) { if err != nil { t.Fatal(err) } - appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON) + appendManifest(arc, os, ocispec.MediaTypeImageManifest, manifestJSON) } generateIndex := func(manifests ...ocispec.Descriptor) { index := ocispec.Index{ @@ -512,15 +511,13 @@ func TestCopy_WithOptions(t *testing.T) { appendBlob(ocispec.MediaTypeImageIndex, indexJSON) } - appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 - appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 - generateManifest("test-arc-1", "test-os-1", "v1", descs[0], descs[1:3]...) // Blob 3 - appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 4 - generateManifest("test-arc-2", "test-os-2", "v1", descs[0], descs[4]) // Blob 5 - appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 6 - generateManifest("test-arc-1", "test-os-1", "v2", descs[0], descs[6]) // Blob 7 - generateIndex(descs[3], descs[5], descs[7]) // Blob 8 + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + generateManifest("test-arc-1", "test-os-1", descs[0], descs[1:3]...) // Blob 3 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 4 + generateManifest("test-arc-2", "test-os-2", descs[0], descs[4]) // Blob 5 + generateIndex(descs[3], descs[5]) // Blob 6 ctx := context.Background() for i := range blobs { @@ -530,7 +527,7 @@ func TestCopy_WithOptions(t *testing.T) { } } - root := descs[8] + root := descs[6] ref := "foobar" err := src.Tag(ctx, root, ref) if err != nil { @@ -580,6 +577,20 @@ func TestCopy_WithOptions(t *testing.T) { preCopyCount := int64(0) postCopyCount := int64(0) opts = oras.CopyOptions{ + MapRoot: func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (ocispec.Descriptor, error) { + manifests, err := content.Successors(ctx, src, root) + if err != nil { + return ocispec.Descriptor{}, errdef.ErrNotFound + } + + // platform filter + for _, m := range manifests { + if m.Platform.Architecture == "test-arc-2" && m.Platform.OS == "test-os-2" { + return m, nil + } + } + return ocispec.Descriptor{}, errdef.ErrNotFound + }, CopyGraphOptions: oras.CopyGraphOptions{ PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error { atomic.AddInt64(&preCopyCount, 1) @@ -591,11 +602,6 @@ func TestCopy_WithOptions(t *testing.T) { }, }, } - targetPlatform := ocispec.Platform{ - Architecture: "test-arc-2", - OS: "test-os-2", - } - opts.AddPlatformFilter(&targetPlatform) wantDesc := descs[5] gotDesc, err = oras.Copy(ctx, src, ref, dst, "", opts) if err != nil { @@ -633,6 +639,136 @@ func TestCopy_WithOptions(t *testing.T) { t.Errorf("count(PostCopy()) = %v, want %v", got, want) } + // test copy with root filter, but no matching node can be found + dst = memory.New() + opts = oras.CopyOptions{ + MapRoot: func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (ocispec.Descriptor, error) { + if root.MediaType == "test" { + return root, nil + } else { + return ocispec.Descriptor{}, errdef.ErrNotFound + } + }, + CopyGraphOptions: oras.DefaultCopyGraphOptions, + } + + _, err = oras.Copy(ctx, src, ref, dst, "", opts) + if !errors.Is(err, errdef.ErrNotFound) { + t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrNotFound) + } +} + +func TestCopy_WithPlatformFilterOptions(t *testing.T) { + src := memory.New() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + 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)), + }) + } + appendManifest := func(arc, os, variant string, mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + Platform: &ocispec.Platform{ + Architecture: arc, + OS: os, + Variant: variant, + }, + }) + } + generateManifest := func(arc, os, variant string, config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendManifest(arc, os, variant, ocispec.MediaTypeImageManifest, manifestJSON) + } + generateIndex := func(manifests ...ocispec.Descriptor) { + index := ocispec.Index{ + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageIndex, indexJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + generateManifest("test-arc-1", "test-os-1", "v1", descs[0], descs[1:3]...) // Blob 3 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 4 + generateManifest("test-arc-2", "test-os-2", "v1", descs[0], descs[4]) // Blob 5 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 6 + generateManifest("test-arc-1", "test-os-1", "v2", descs[0], descs[6]) // Blob 7 + generateIndex(descs[3], descs[5], descs[7]) // Blob 8 + + ctx := context.Background() + for i := range blobs { + err := src.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + root := descs[8] + ref := "foobar" + err := src.Tag(ctx, root, ref) + if err != nil { + t.Fatal("fail to tag root node", err) + } + + // test copy with platform filter + dst := memory.New() + opts := oras.CopyOptions{} + targetPlatform := ocispec.Platform{ + Architecture: "test-arc-2", + OS: "test-os-2", + } + opts.WithPlatformFilter(&targetPlatform) + wantDesc := descs[5] + gotDesc, err := oras.Copy(ctx, src, ref, dst, "", opts) + if err != nil { + t.Fatalf("Copy() error = %v, wantErr %v", err, false) + } + if !reflect.DeepEqual(gotDesc, wantDesc) { + t.Errorf("Copy() = %v, want %v", gotDesc, wantDesc) + } + + // verify contents + for i, desc := range append([]ocispec.Descriptor{descs[0]}, descs[4:6]...) { + exists, err := dst.Exists(ctx, desc) + if err != nil { + t.Fatalf("dst.Exists(%d) error = %v", i, err) + } + if !exists { + t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true) + } + } + + // verify tag + gotDesc, err = dst.Resolve(ctx, ref) + if err != nil { + t.Fatal("dst.Resolve() error =", err) + } + if !reflect.DeepEqual(gotDesc, wantDesc) { + t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc) + } + // test copy with platform filter, if the multiple manifests match the required platform, // return the first matching entry dst = memory.New() @@ -641,7 +777,7 @@ func TestCopy_WithOptions(t *testing.T) { OS: "test-os-1", } opts = oras.CopyOptions{} - opts.AddPlatformFilter(&targetPlatform) + opts.WithPlatformFilter(&targetPlatform) wantDesc = descs[3] gotDesc, err = oras.Copy(ctx, src, ref, dst, "", opts) if err != nil { @@ -671,7 +807,8 @@ func TestCopy_WithOptions(t *testing.T) { t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc) } - // test copy with platform filter and exisiting MapRoot func, but no matching node can be found + // test copy with platform filter and existing MapRoot func, but no matching node can be found + // should return not found error dst = memory.New() opts = oras.CopyOptions{ MapRoot: func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (ocispec.Descriptor, error) { @@ -686,29 +823,32 @@ func TestCopy_WithOptions(t *testing.T) { Architecture: "test-arc-1", OS: "test-os-3", } - opts.AddPlatformFilter(&targetPlatform) + opts.WithPlatformFilter(&targetPlatform) _, err = oras.Copy(ctx, src, ref, dst, "", opts) if !errors.Is(err, errdef.ErrNotFound) { t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrNotFound) } - // test copy with root filter, but no matching node can be found + // test copy with platform filter, but the node's media type is not supported + // should return unsupported error dst = memory.New() - opts = oras.CopyOptions{ - MapRoot: func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (ocispec.Descriptor, error) { - if root.MediaType == "test" { - return root, nil - } else { - return ocispec.Descriptor{}, errdef.ErrNotFound - } - }, - CopyGraphOptions: oras.DefaultCopyGraphOptions, + opts = oras.CopyOptions{} + targetPlatform = ocispec.Platform{ + Architecture: "test-arc-1", + OS: "test-os-1", + } + opts.WithPlatformFilter(&targetPlatform) + + root = descs[7] + err = src.Tag(ctx, root, ref) + if err != nil { + t.Fatal("fail to tag root node", err) } _, err = oras.Copy(ctx, src, ref, dst, "", opts) - if !errors.Is(err, errdef.ErrNotFound) { - t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrNotFound) + if !errors.Is(err, errdef.ErrUnsupported) { + t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrUnsupported) } } diff --git a/internal/platform/platform.go b/internal/platform/platform.go index 35d2777d..d485d850 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -27,29 +27,29 @@ import ( // of the current platform. // Note: Variant, OSVersion and OSFeatures are optional fields, will skip the // comparison if the target platform does not provide specfic value. -func MatchPlatform(curr *ocispec.Platform, target *ocispec.Platform) bool { - if curr.Architecture != target.Architecture || curr.OS != target.OS { +func MatchPlatform(got *ocispec.Platform, want *ocispec.Platform) bool { + if got.Architecture != want.Architecture || got.OS != want.OS { return false } - if target.OSVersion != "" && curr.OSVersion != target.OSVersion { + if want.OSVersion != "" && got.OSVersion != want.OSVersion { return false } - if target.Variant != "" && curr.Variant != target.Variant { + if want.Variant != "" && got.Variant != want.Variant { return false } - if len(target.OSFeatures) != 0 && !isSubset(target.OSFeatures, curr.OSFeatures) { + if len(want.OSFeatures) != 0 && !isSubset(want.OSFeatures, got.OSFeatures) { return false } return true } -// isSubset returns true if all items in slice A are present in slice B +// isSubset returns true if all items in slice A are present in slice B. func isSubset(a, b []string) bool { - set := make(map[string]bool) + set := make(map[string]bool, len(b)) for _, v := range b { set[v] = true }