Skip to content

Commit

Permalink
[PRFix modify platform selection, and optimize resolve func]
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 4, 2022
1 parent be121e9 commit 8be0e77
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 54 deletions.
52 changes: 38 additions & 14 deletions content.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ package oras

import (
"context"
"fmt"

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

Expand All @@ -44,33 +48,53 @@ func Tag(ctx context.Context, target Target, src, dst string) error {
return target.Tag(ctx, desc, dst)
}

var (
// DefaultResolveOptions provides the default ResolveOptions.
DefaultResolveOptions = ResolveOptions{
TargetPlatform: nil,
}
)
// DefaultResolveOptions provides the default ResolveOptions.
var DefaultResolveOptions ResolveOptions

// ResolveOptions contains parameters for oras.Resolve.
type ResolveOptions struct {
// TargetPlatform is the target platform.
// Will do the platform selection if specified.
// TargetPlatform ensures the resolved content matches the target platform
// if the node is a manifest, or selects the first resolved content that
// matches the target platform if the node is a manifest list.
TargetPlatform *ocispec.Platform
}

// Resolve resolves a descriptor with provided reference from the target.
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.TargetPlatform == nil {
return target.Resolve(ctx, ref)
}

if opts.TargetPlatform != nil {
desc, err = selectPlatform(ctx, target, desc, opts.TargetPlatform)
if refFetcher, ok := target.(registry.ReferenceFetcher); ok {
// optimize performance for ReferenceFetcher targets
desc, rc, err := refFetcher.FetchReference(ctx, ref)
if err != nil {
return ocispec.Descriptor{}, err
}
defer rc.Close()

switch desc.MediaType {
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex,
docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
store := cas.NewMemory()
err = store.Push(ctx, desc, rc)
if err != nil {
return ocispec.Descriptor{}, err
}

// create a proxy for the target storage, use the store as the cache
proxy := cas.NewProxy(target, store)
proxy.StopCaching = true
return selectPlatform(ctx, proxy, desc, opts.TargetPlatform)
default:
return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrUnsupported)
}
}

desc, err := target.Resolve(ctx, ref)
if err != nil {
return ocispec.Descriptor{}, err
}

return desc, nil
return selectPlatform(ctx, target, desc, opts.TargetPlatform)
}
98 changes: 95 additions & 3 deletions content_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ func TestResolve_WithOptions(t *testing.T) {
t.Fatal("fail to tag manifestDesc node", err)
}

// test Resolve with TargetPlatform
// test Resolve with default resolve options
resolveOptions := oras.DefaultResolveOptions
gotDesc, err := oras.Resolve(ctx, target, ref, resolveOptions)

Expand All @@ -247,7 +247,7 @@ func TestResolve_WithOptions(t *testing.T) {
}
}

func TestResolve_WithTargetPlatformOptions(t *testing.T) {
func TestResolve_Memory_WithTargetPlatformOptions(t *testing.T) {
target := memory.New()
arc_1 := "test-arc-1"
os_1 := "test-os-1"
Expand Down Expand Up @@ -295,7 +295,7 @@ func TestResolve_WithTargetPlatformOptions(t *testing.T) {
"author":"test author",
"architecture":"test-arc-1",
"os":"test-os-1",
"variant":"test-variant"}`)) // Blob 0
"variant":"v1"}`)) // Blob 0
appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1
appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2
generateManifest(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3
Expand Down Expand Up @@ -345,3 +345,95 @@ func TestResolve_WithTargetPlatformOptions(t *testing.T) {
t.Fatalf("oras.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}

func TestResolve_Repository_WithTargetPlatformOptions(t *testing.T) {
arc_1 := "test-arc-1"
arc_2 := "test-arc-2"
os_1 := "test-os-1"
var digest_1 digest.Digest = "sha256:11ec3af9dfeb49c89ef71877ba85249be527e4dda9d1d74d99dc618d1a5fa151"

manifestDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageManifest,
Digest: digest_1,
Size: 484,
Platform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}

index := []byte(`{"manifests":[{
"mediaType":"application/vnd.oci.image.manifest.v1+json",
"digest":"sha256:11ec3af9dfeb49c89ef71877ba85249be527e4dda9d1d74d99dc618d1a5fa151",
"size":484,
"platform":{"architecture":"test-arc-1","os":"test-os-1"}},{
"mediaType":"application/vnd.oci.image.manifest.v1+json",
"digest":"sha256:b955aefa63749f07fad84ab06a45a951368e3ac79799bc44a158fac1bb8ca208",
"size":337,
"platform":{"architecture":"test-arc-2","os":"test-os-2"}}]}`)
indexDesc := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageIndex,
Digest: digest.FromBytes(index),
Size: int64(len(index)),
}
src := "foobar"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && (r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String() || r.URL.Path == "/v2/test/manifests/"+src):
if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) {
t.Errorf("manifest not convertable: %s", accept)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", indexDesc.MediaType)
w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String())
if _, err := w.Write(index); err != nil {
t.Errorf("failed to write %q: %v", r.URL, err)
}
default:
t.Errorf("unexpected access: %s %s", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}

repoName := uri.Host + "/test"
repo, err := remote.NewRepository(repoName)
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()

// test Resolve with TargetPlatform
resolveOptions := oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: os_1,
},
}
gotDesc, err := oras.Resolve(ctx, repo, src, resolveOptions)
if err != nil {
t.Fatal("oras.Resolve() error =", err)
}
if !reflect.DeepEqual(gotDesc, manifestDesc) {
t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc)
}

// test Resolve with TargetPlatform but there is no matching node
// Should return not found error
resolveOptions = oras.ResolveOptions{
TargetPlatform: &ocispec.Platform{
Architecture: arc_1,
OS: arc_2,
},
}
_, err = oras.Resolve(ctx, repo, src, resolveOptions)
if !errors.Is(err, errdef.ErrNotFound) {
t.Fatalf("oras.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound)
}
}
61 changes: 40 additions & 21 deletions copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,30 @@ type CopyOptions struct {
MapRoot func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (ocispec.Descriptor, error)
}

// selectPlatform implements platform filter and returns the descriptor of
// 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.
// getPlatformFromConfig returns a platform object which is made up from the
// fields in config blob.
func getPlatformFromConfig(ctx context.Context, src content.Storage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) {
if desc.MediaType != targetConfigMediaType {
return nil, fmt.Errorf("mismatch MediaType %s: expect %s", desc.MediaType, targetConfigMediaType)
}

rc, err := src.Fetch(ctx, desc)
if err != nil {
return nil, err
}
defer rc.Close()

var platform *ocispec.Platform
if err = json.NewDecoder(rc).Decode(&platform); err != nil {
return nil, err
}

return platform, nil
}

// selectPlatform implements platform filter and returns the descriptor of the
// first matched manifest if the root is a manifest list. 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) {
switch root.MediaType {
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
Expand All @@ -82,31 +102,30 @@ func selectPlatform(ctx context.Context, src content.Storage, root ocispec.Descr
return ocispec.Descriptor{}, err
}

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
}
defer rc.Close()
var currPlatform ocispec.Platform
if err = json.NewDecoder(rc).Decode(&currPlatform); err != nil {
return ocispec.Descriptor{}, err
}
configMediaType := docker.MediaTypeConfig
if root.MediaType == ocispec.MediaTypeImageManifest {
configMediaType = ocispec.MediaTypeImageConfig
}

if platform.Match(&currPlatform, p) {
return root, nil
}
}
cfgPlatform, err := getPlatformFromConfig(ctx, src, descs[0], configMediaType)
if err != nil {
return ocispec.Descriptor{}, err
}
if cfgPlatform == nil {
return ocispec.Descriptor{}, fmt.Errorf("%s: missing platform info", descs[0].Digest)
}

if platform.Match(cfgPlatform, p) {
return root, nil
}
return ocispec.Descriptor{}, errdef.ErrNotFound
default:
return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", root.Digest, root.MediaType, errdef.ErrUnsupported)
}
}

// WithPlatformFilter adds the check on the platform attributes.
func (o *CopyOptions) WithPlatformFilter(p *ocispec.Platform) {
// WithTargetPlatform adds the check on the platform attributes.
func (o *CopyOptions) WithTargetPlatform(p *ocispec.Platform) {
mapRoot := o.MapRoot
o.MapRoot = func(ctx context.Context, src content.Storage, root ocispec.Descriptor) (desc ocispec.Descriptor, err error) {
if mapRoot != nil {
Expand Down
Loading

0 comments on commit 8be0e77

Please sign in to comment.