From 6fa3900c243c1a9131f04ac16eef4411133a1649 Mon Sep 17 00:00:00 2001 From: Zoey Li Date: Mon, 1 Aug 2022 13:52:49 +0800 Subject: [PATCH] [PRFix add Resolve and support manifest platform selection] Signed-off-by: Zoey Li --- content.go | 25 +++++++++ copy.go | 55 +++++++++++++++----- copy_test.go | 83 +++++++++++++++++++++++++++--- internal/docker/mediatype.go | 1 + internal/platform/platform.go | 14 ++--- internal/platform/platform_test.go | 2 +- 6 files changed, 151 insertions(+), 29 deletions(-) diff --git a/content.go b/content.go index 7e15fbd8..2fb52c20 100644 --- a/content.go +++ b/content.go @@ -18,6 +18,7 @@ package oras import ( "context" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/registry" ) @@ -42,3 +43,27 @@ func Tag(ctx context.Context, target Target, src, dst string) error { } return target.Tag(ctx, desc, dst) } + +// ResolveOptions contains parameters for oras.Resolve. +type ResolveOptions struct { + // MatchPlaform is the target platform. + // Will do the platform selection if specified. + MatchPlatform *ocispec.Platform +} + +// Resolve returns the resolved descriptor. +func Resolve(ctx context.Context, target Target, ref string, opts ResolveOptions) (ocispec.Descriptor, error) { + desc, err := target.Resolve(ctx, ref) + if err != nil { + return ocispec.Descriptor{}, err + } + + if opts.MatchPlatform != nil { + desc, err = selectPlatform(ctx, target, desc, opts.MatchPlatform) + if err != nil { + return ocispec.Descriptor{}, err + } + } + + return desc, nil +} diff --git a/copy.go b/copy.go index 1a9f66a7..027c983e 100644 --- a/copy.go +++ b/copy.go @@ -17,6 +17,7 @@ package oras import ( "context" + "encoding/json" "errors" "fmt" "io" @@ -57,24 +58,52 @@ type CopyOptions struct { } // selectPlatform implements platform filter and returns the descriptor of -// the first matched manifest from the manifest list / image index. +// the first matched manifest if the root is a manifest list / image index. +// If the root is a manifest, then return the root descriptor if platform +// matches. 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) - } + switch root.MediaType { + case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex: + manifests, err := content.Successors(ctx, src, root) + if err != nil { + return ocispec.Descriptor{}, err + } - manifests, err := content.Successors(ctx, src, root) - if err != nil { - return ocispec.Descriptor{}, err - } + // platform filter + for _, m := range manifests { + if platform.Match(m.Platform, p) { + return m, nil + } + } + return ocispec.Descriptor{}, errdef.ErrNotFound + case docker.MediaTypeManifest, ocispec.MediaTypeImageManifest: + descs, 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 + for _, desc := range descs { + if desc.MediaType == docker.MediaTypeImage || desc.MediaType == ocispec.MediaTypeImageConfig { + rc, err := src.Fetch(ctx, desc) + if err != nil { + return ocispec.Descriptor{}, err + } + var currPlatform ocispec.Platform + err = json.NewDecoder(rc).Decode(&currPlatform) + if err != nil { + return ocispec.Descriptor{}, err + } + defer rc.Close() + + if platform.Match(&currPlatform, p) { + return root, nil + } + } } + return ocispec.Descriptor{}, errdef.ErrNotFound + default: + return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported) } - return ocispec.Descriptor{}, errdef.ErrNotFound } // WithPlatformFilter adds the check on the platform attributes. diff --git a/copy_test.go b/copy_test.go index ede33050..8d56c41e 100644 --- a/copy_test.go +++ b/copy_test.go @@ -707,7 +707,12 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { appendBlob(ocispec.MediaTypeImageIndex, indexJSON) } - appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json", +"created":"2022-07-29T08:13:55Z", +"author":"test author", +"architecture":"test-arc-1", +"os":"test-os-1", +"variant":"test-variant"}`)) // 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 @@ -732,7 +737,7 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { t.Fatal("fail to tag root node", err) } - // test copy with platform filter + // test copy with platform filter for the image index dst := memory.New() opts := oras.CopyOptions{} targetPlatform := ocispec.Platform{ @@ -769,8 +774,9 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { 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 + // test copy with platform filter for the image index, and multiple + // manifests match the required platform. Should return the first + // matching entry. dst = memory.New() targetPlatform = ocispec.Platform{ Architecture: "test-arc-1", @@ -807,8 +813,9 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { t.Errorf("dst.Resolve() = %v, want %v", gotDesc, wantDesc) } - // test copy with platform filter and existing MapRoot func, but no matching node can be found - // should return not found error + // test copy with platform filter and existing MapRoot func for the + // image index, but there is no matching node. 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) { @@ -830,8 +837,7 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrNotFound) } - // test copy with platform filter, but the node's media type is not supported - // should return unsupported error + // test copy with platform filter for the manifest dst = memory.New() opts = oras.CopyOptions{} targetPlatform = ocispec.Platform{ @@ -846,6 +852,67 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { t.Fatal("fail to tag root node", err) } + wantDesc = descs[7] + 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[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 for the manifest, but there is no + // matching node. Should return not found error. + dst = memory.New() + opts = oras.CopyOptions{} + targetPlatform = ocispec.Platform{ + Architecture: "test-arc-1", + OS: "test-os-1", + Variant: "wrong-variant", + } + 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 platform filter, but the node's media type is not + // supported. Should return unsupported error + dst = memory.New() + opts = oras.CopyOptions{} + targetPlatform = ocispec.Platform{ + Architecture: "test-arc-1", + OS: "test-os-1", + } + opts.WithPlatformFilter(&targetPlatform) + + root = descs[1] + 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.ErrUnsupported) { t.Fatalf("Copy() error = %v, wantErr %v", err, errdef.ErrUnsupported) diff --git a/internal/docker/mediatype.go b/internal/docker/mediatype.go index f88a422a..5febba7c 100644 --- a/internal/docker/mediatype.go +++ b/internal/docker/mediatype.go @@ -17,6 +17,7 @@ package docker // docker media types const ( + MediaTypeImage = "application/vnd.docker.container.image.v1+json" MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" ) diff --git a/internal/platform/platform.go b/internal/platform/platform.go index d485d850..d3f030bf 100644 --- a/internal/platform/platform.go +++ b/internal/platform/platform.go @@ -19,15 +19,15 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) -// MatchPlatform checks whether the current platform matches the target platform. -// MatchPlatform will return true if all of the following conditions are met. +// Match checks whether the current platform matches the target platform. +// Match will return true if all of the following conditions are met. // - Architecture and OS exactly match. // - Variant and OSVersion exactly match if target platform provided. -// - OSFeatures of the target platform are the subsets of the OSFeatures array -// 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(got *ocispec.Platform, want *ocispec.Platform) bool { +// - OSFeatures of the target platform are the subsets of the OSFeatures +// array 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 Match(got *ocispec.Platform, want *ocispec.Platform) bool { if got.Architecture != want.Architecture || got.OS != want.OS { return false } diff --git a/internal/platform/platform_test.go b/internal/platform/platform_test.go index 9ce3a31d..9e7cc1e3 100644 --- a/internal/platform/platform_test.go +++ b/internal/platform/platform_test.go @@ -94,7 +94,7 @@ func TestMatchPlatform(t *testing.T) { targetPlatforJSON, _ := json.Marshal(tt.target) name := string(currPlatforJSON) + string(targetPlatforJSON) t.Run(name, func(t *testing.T) { - if got := MatchPlatform(&tt.curr, &tt.target); got != tt.want { + if got := Match(&tt.curr, &tt.target); got != tt.want { t.Errorf("MatchPlatform() = %v, want %v", got, tt.want) } })