Skip to content

Commit

Permalink
[PRFix add Resolve and support manifest platform selection]
Browse files Browse the repository at this point in the history
Signed-off-by: Zoey Li <zoeyli@microsoft.com>
  • Loading branch information
lizMSFT committed Aug 2, 2022
1 parent 22e266b commit 219d676
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 30 deletions.
25 changes: 25 additions & 0 deletions content.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package oras
import (
"context"

ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/registry"
)

Expand All @@ -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
}
55 changes: 42 additions & 13 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package oras

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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.
Expand Down
83 changes: 75 additions & 8 deletions copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand All @@ -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{
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions internal/docker/mediatype.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
14 changes: 7 additions & 7 deletions internal/platform/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions internal/platform/platform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

func TestMatchPlatform(t *testing.T) {
func TestMatch(t *testing.T) {
tests := []struct {
curr ocispec.Platform
target ocispec.Platform
Expand Down Expand Up @@ -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)
}
})
Expand Down

0 comments on commit 219d676

Please sign in to comment.