diff --git a/content.go b/content.go index 2fb52c20a..4fefc1d58 100644 --- a/content.go +++ b/content.go @@ -44,22 +44,29 @@ 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, + } +) + // ResolveOptions contains parameters for oras.Resolve. type ResolveOptions struct { - // MatchPlaform is the target platform. + // TargetPlatform is the target platform. // Will do the platform selection if specified. - MatchPlatform *ocispec.Platform + TargetPlatform *ocispec.Platform } -// Resolve returns the resolved descriptor. +// 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.MatchPlatform != nil { - desc, err = selectPlatform(ctx, target, desc, opts.MatchPlatform) + if opts.TargetPlatform != nil { + desc, err = selectPlatform(ctx, target, desc, opts.TargetPlatform) if err != nil { return ocispec.Descriptor{}, err } diff --git a/content_test.go b/content_test.go index ffd72a828..01c245312 100644 --- a/content_test.go +++ b/content_test.go @@ -19,6 +19,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" "net/http/httptest" "net/url" @@ -30,6 +31,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2" "oras.land/oras-go/v2/content/memory" + "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/registry/remote" ) @@ -186,3 +188,160 @@ func TestTag_Repository(t *testing.T) { t.Errorf("Repository.TagReference() = %v, want %v", gotIndex, index) } } + +func TestResolve_WithOptions(t *testing.T) { + target := 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)), + }) + } + generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 + generateManifest(descs[0], descs[1:3]...) // Blob 3 + + ctx := context.Background() + for i := range blobs { + err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + manifestDesc := descs[3] + ref := "foobar" + err := target.Tag(ctx, manifestDesc, ref) + if err != nil { + t.Fatal("fail to tag manifestDesc node", err) + } + + // test Resolve with TargetPlatform + resolveOptions := oras.DefaultResolveOptions + gotDesc, err := oras.Resolve(ctx, target, ref, resolveOptions) + + if err != nil { + t.Fatal("oras.Resolve() error =", err) + } + if !reflect.DeepEqual(gotDesc, manifestDesc) { + t.Errorf("oras.Resolve() = %v, want %v", gotDesc, manifestDesc) + } +} + +func TestResolve_WithTargetPlatformOptions(t *testing.T) { + target := memory.New() + arc_1 := "test-arc-1" + os_1 := "test-os-1" + variant_1 := "v1" + variant_2 := "v2" + + // 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) + } + + 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(arc_1, os_1, variant_1, descs[0], descs[1:3]...) // Blob 3 + + ctx := context.Background() + for i := range blobs { + err := target.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + + manifestDesc := descs[3] + ref := "foobar" + err := target.Tag(ctx, manifestDesc, ref) + if err != nil { + t.Fatal("fail to tag manifestDesc node", err) + } + + // test Resolve with TargetPlatform + resolveOptions := oras.ResolveOptions{ + TargetPlatform: &ocispec.Platform{ + Architecture: arc_1, + OS: os_1, + }, + } + gotDesc, err := oras.Resolve(ctx, target, ref, 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: os_1, + Variant: variant_2, + }, + } + _, err = oras.Resolve(ctx, target, ref, resolveOptions) + if !errors.Is(err, errdef.ErrNotFound) { + t.Fatalf("oras.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) + } +} diff --git a/copy.go b/copy.go index 027c983e3..58a750473 100644 --- a/copy.go +++ b/copy.go @@ -88,12 +88,11 @@ func selectPlatform(ctx context.Context, src content.Storage, root ocispec.Descr if err != nil { return ocispec.Descriptor{}, err } + defer rc.Close() var currPlatform ocispec.Platform - err = json.NewDecoder(rc).Decode(&currPlatform) - if err != nil { + if err = json.NewDecoder(rc).Decode(&currPlatform); err != nil { return ocispec.Descriptor{}, err } - defer rc.Close() if platform.Match(&currPlatform, p) { return root, nil diff --git a/copy_test.go b/copy_test.go index 8d56c41e3..1127db4d3 100644 --- a/copy_test.go +++ b/copy_test.go @@ -660,6 +660,12 @@ func TestCopy_WithOptions(t *testing.T) { func TestCopy_WithPlatformFilterOptions(t *testing.T) { src := memory.New() + arc_1 := "test-arc-1" + os_1 := "test-os-1" + variant_1 := "v1" + arc_2 := "test-arc-2" + os_2 := "test-os-2" + variant_2 := "v2" // generate test content var blobs [][]byte @@ -713,14 +719,14 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { "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 - 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.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 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1")) // Blob 4 + generateManifest(arc_2, os_2, variant_1, descs[0], descs[4]) // Blob 5 + appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2")) // Blob 6 + generateManifest(arc_1, os_1, variant_2, descs[0], descs[6]) // Blob 7 + generateIndex(descs[3], descs[5], descs[7]) // Blob 8 ctx := context.Background() for i := range blobs { @@ -741,8 +747,8 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { dst := memory.New() opts := oras.CopyOptions{} targetPlatform := ocispec.Platform{ - Architecture: "test-arc-2", - OS: "test-os-2", + Architecture: arc_2, + OS: os_2, } opts.WithPlatformFilter(&targetPlatform) wantDesc := descs[5] @@ -779,8 +785,8 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { // matching entry. dst = memory.New() targetPlatform = ocispec.Platform{ - Architecture: "test-arc-1", - OS: "test-os-1", + Architecture: arc_1, + OS: os_1, } opts = oras.CopyOptions{} opts.WithPlatformFilter(&targetPlatform) @@ -827,8 +833,8 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { }, } targetPlatform = ocispec.Platform{ - Architecture: "test-arc-1", - OS: "test-os-3", + Architecture: arc_1, + OS: os_2, } opts.WithPlatformFilter(&targetPlatform) @@ -841,8 +847,8 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { dst = memory.New() opts = oras.CopyOptions{} targetPlatform = ocispec.Platform{ - Architecture: "test-arc-1", - OS: "test-os-1", + Architecture: arc_1, + OS: os_1, } opts.WithPlatformFilter(&targetPlatform) @@ -886,9 +892,9 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { dst = memory.New() opts = oras.CopyOptions{} targetPlatform = ocispec.Platform{ - Architecture: "test-arc-1", - OS: "test-os-1", - Variant: "wrong-variant", + Architecture: arc_1, + OS: os_1, + Variant: variant_2, } opts.WithPlatformFilter(&targetPlatform) @@ -902,8 +908,8 @@ func TestCopy_WithPlatformFilterOptions(t *testing.T) { dst = memory.New() opts = oras.CopyOptions{} targetPlatform = ocispec.Platform{ - Architecture: "test-arc-1", - OS: "test-os-1", + Architecture: arc_1, + OS: os_1, } opts.WithPlatformFilter(&targetPlatform) diff --git a/internal/platform/platform_test.go b/internal/platform/platform_test.go index 9b32fc018..d3b8a21bb 100644 --- a/internal/platform/platform_test.go +++ b/internal/platform/platform_test.go @@ -24,9 +24,9 @@ import ( func TestMatch(t *testing.T) { tests := []struct { - curr ocispec.Platform - target ocispec.Platform - want bool + got ocispec.Platform + want ocispec.Platform + isMatched bool }{{ ocispec.Platform{Architecture: "amd64", OS: "linux"}, ocispec.Platform{Architecture: "amd64", OS: "linux"}, @@ -90,12 +90,12 @@ func TestMatch(t *testing.T) { }} for _, tt := range tests { - currPlatforJSON, _ := json.Marshal(tt.curr) - targetPlatforJSON, _ := json.Marshal(tt.target) - name := string(currPlatforJSON) + string(targetPlatforJSON) + gotPlatformJSON, _ := json.Marshal(tt.got) + wantPlatformJSON, _ := json.Marshal(tt.want) + name := string(gotPlatformJSON) + string(wantPlatformJSON) t.Run(name, func(t *testing.T) { - if got := Match(&tt.curr, &tt.target); got != tt.want { - t.Errorf("MatchPlatform() = %v, want %v", got, tt.want) + if actual := Match(&tt.got, &tt.want); actual != tt.isMatched { + t.Errorf("Match() = %v, want %v", actual, tt.isMatched) } }) }