oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/internal/platform/platform_test.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package platform
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	_ "crypto/sha256"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"reflect"
    26  	"testing"
    27  
    28  	"github.com/opencontainers/go-digest"
    29  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    30  	"oras.land/oras-go/v2/errdef"
    31  	"oras.land/oras-go/v2/internal/cas"
    32  	"oras.land/oras-go/v2/internal/docker"
    33  )
    34  
    35  func TestMatch(t *testing.T) {
    36  	tests := []struct {
    37  		got       ocispec.Platform
    38  		want      ocispec.Platform
    39  		isMatched bool
    40  	}{{
    41  		ocispec.Platform{Architecture: "amd64", OS: "linux"},
    42  		ocispec.Platform{Architecture: "amd64", OS: "linux"},
    43  		true,
    44  	}, {
    45  		ocispec.Platform{Architecture: "amd64", OS: "linux"},
    46  		ocispec.Platform{Architecture: "amd64", OS: "LINUX"},
    47  		false,
    48  	}, {
    49  		ocispec.Platform{Architecture: "amd64", OS: "linux"},
    50  		ocispec.Platform{Architecture: "arm64", OS: "linux"},
    51  		false,
    52  	}, {
    53  		ocispec.Platform{Architecture: "arm", OS: "linux"},
    54  		ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
    55  		false,
    56  	}, {
    57  		ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
    58  		ocispec.Platform{Architecture: "arm", OS: "linux"},
    59  		true,
    60  	}, {
    61  		ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
    62  		ocispec.Platform{Architecture: "arm", OS: "linux", Variant: "v7"},
    63  		true,
    64  	}, {
    65  		ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
    66  		ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.700"},
    67  		false,
    68  	}, {
    69  		ocispec.Platform{Architecture: "amd64", OS: "windows"},
    70  		ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
    71  		false,
    72  	}, {
    73  		ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
    74  		ocispec.Platform{Architecture: "amd64", OS: "windows"},
    75  		true,
    76  	}, {
    77  		ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
    78  		ocispec.Platform{Architecture: "amd64", OS: "windows", OSVersion: "10.0.20348.768"},
    79  		true,
    80  	}, {
    81  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "d"}},
    82  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "c"}},
    83  		false,
    84  	}, {
    85  		ocispec.Platform{Architecture: "arm", OS: "linux"},
    86  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a"}},
    87  		false,
    88  	}, {
    89  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a"}},
    90  		ocispec.Platform{Architecture: "arm", OS: "linux"},
    91  		true,
    92  	}, {
    93  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "b"}},
    94  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "b"}},
    95  		true,
    96  	}, {
    97  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"a", "d", "c", "b"}},
    98  		ocispec.Platform{Architecture: "arm", OS: "linux", OSFeatures: []string{"d", "c", "a", "b"}},
    99  		true,
   100  	}}
   101  
   102  	for _, tt := range tests {
   103  		gotPlatformJSON, _ := json.Marshal(tt.got)
   104  		wantPlatformJSON, _ := json.Marshal(tt.want)
   105  		name := string(gotPlatformJSON) + string(wantPlatformJSON)
   106  		t.Run(name, func(t *testing.T) {
   107  			if actual := Match(&tt.got, &tt.want); actual != tt.isMatched {
   108  				t.Errorf("Match() = %v, want %v", actual, tt.isMatched)
   109  			}
   110  		})
   111  	}
   112  }
   113  
   114  func TestSelectManifest(t *testing.T) {
   115  	storage := cas.NewMemory()
   116  	arc_1 := "test-arc-1"
   117  	os_1 := "test-os-1"
   118  	variant_1 := "v1"
   119  	arc_2 := "test-arc-2"
   120  	os_2 := "test-os-2"
   121  	variant_2 := "v2"
   122  
   123  	// generate test content
   124  	var blobs [][]byte
   125  	var descs []ocispec.Descriptor
   126  	appendBlob := func(mediaType string, blob []byte) {
   127  		blobs = append(blobs, blob)
   128  		descs = append(descs, ocispec.Descriptor{
   129  			MediaType: mediaType,
   130  			Digest:    digest.FromBytes(blob),
   131  			Size:      int64(len(blob)),
   132  		})
   133  	}
   134  	appendManifest := func(arc, os, variant string, hasPlatform bool, mediaType string, blob []byte) {
   135  		blobs = append(blobs, blob)
   136  		desc := ocispec.Descriptor{
   137  			MediaType: mediaType,
   138  			Digest:    digest.FromBytes(blob),
   139  			Size:      int64(len(blob)),
   140  		}
   141  		if hasPlatform {
   142  			desc.Platform = &ocispec.Platform{
   143  				Architecture: arc,
   144  				OS:           os,
   145  				Variant:      variant,
   146  			}
   147  		}
   148  		descs = append(descs, desc)
   149  	}
   150  	generateManifest := func(arc, os, variant string, hasPlatform bool, subject *ocispec.Descriptor, config ocispec.Descriptor, layers ...ocispec.Descriptor) {
   151  		manifest := ocispec.Manifest{
   152  			Subject: subject,
   153  			Config:  config,
   154  			Layers:  layers,
   155  		}
   156  		manifestJSON, err := json.Marshal(manifest)
   157  		if err != nil {
   158  			t.Fatal(err)
   159  		}
   160  		appendManifest(arc, os, variant, hasPlatform, ocispec.MediaTypeImageManifest, manifestJSON)
   161  	}
   162  	generateIndex := func(subject *ocispec.Descriptor, manifests ...ocispec.Descriptor) {
   163  		index := ocispec.Index{
   164  			Subject:   subject,
   165  			Manifests: manifests,
   166  		}
   167  		indexJSON, err := json.Marshal(index)
   168  		if err != nil {
   169  			t.Fatal(err)
   170  		}
   171  		appendBlob(ocispec.MediaTypeImageIndex, indexJSON)
   172  	}
   173  
   174  	appendBlob("test/subject", []byte("dummy subject")) // Blob 0
   175  	appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
   176  "created":"2022-07-29T08:13:55Z",
   177  "author":"test author",
   178  "architecture":"test-arc-1",
   179  "os":"test-os-1",
   180  "variant":"v1"}`)) // Blob 1
   181  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foo"))                             // Blob 2
   182  	appendBlob(ocispec.MediaTypeImageLayer, []byte("bar"))                             // Blob 3
   183  	generateManifest(arc_1, os_1, variant_1, true, &descs[0], descs[1], descs[2:4]...) // Blob 4
   184  	appendBlob(ocispec.MediaTypeImageLayer, []byte("hello1"))                          // Blob 5
   185  	generateManifest(arc_2, os_2, variant_1, true, nil, descs[1], descs[5])            // Blob 6
   186  	appendBlob(ocispec.MediaTypeImageLayer, []byte("hello2"))                          // Blob 7
   187  	generateManifest(arc_1, os_1, variant_2, true, nil, descs[1], descs[7])            // Blob 8
   188  	generateIndex(&descs[0], descs[4], descs[6], descs[8])                             // Blob 9
   189  
   190  	ctx := context.Background()
   191  	for i := range blobs {
   192  		err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
   193  		if err != nil {
   194  			t.Fatalf("failed to push test content to src: %d: %v", i, err)
   195  		}
   196  	}
   197  
   198  	// test SelectManifest on image index, only one matching manifest found
   199  	root := descs[9]
   200  	targetPlatform := ocispec.Platform{
   201  		Architecture: arc_2,
   202  		OS:           os_2,
   203  	}
   204  	wantDesc := descs[6]
   205  	gotDesc, err := SelectManifest(ctx, storage, root, &targetPlatform)
   206  	if err != nil {
   207  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
   208  	}
   209  	if !reflect.DeepEqual(gotDesc, wantDesc) {
   210  		t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
   211  	}
   212  
   213  	// test SelectManifest on image index,
   214  	// and multiple manifests match the required platform.
   215  	// Should return the first matching entry.
   216  	targetPlatform = ocispec.Platform{
   217  		Architecture: arc_1,
   218  		OS:           os_1,
   219  	}
   220  	wantDesc = descs[4]
   221  	gotDesc, err = SelectManifest(ctx, storage, root, &targetPlatform)
   222  	if err != nil {
   223  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
   224  	}
   225  	if !reflect.DeepEqual(gotDesc, wantDesc) {
   226  		t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
   227  	}
   228  
   229  	// test SelectManifest on manifest
   230  	root = descs[8]
   231  	targetPlatform = ocispec.Platform{
   232  		Architecture: arc_1,
   233  		OS:           os_1,
   234  	}
   235  	wantDesc = descs[8]
   236  	gotDesc, err = SelectManifest(ctx, storage, root, &targetPlatform)
   237  	if err != nil {
   238  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
   239  	}
   240  	if !reflect.DeepEqual(gotDesc, wantDesc) {
   241  		t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
   242  	}
   243  
   244  	// test SelectManifest on manifest, but there is no matching node.
   245  	// Should return not found error.
   246  	root = descs[8]
   247  	targetPlatform = ocispec.Platform{
   248  		Architecture: arc_1,
   249  		OS:           os_1,
   250  		Variant:      variant_2,
   251  	}
   252  	_, err = SelectManifest(ctx, storage, root, &targetPlatform)
   253  	expected := fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
   254  	if err.Error() != expected {
   255  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
   256  	}
   257  
   258  	// test SelectManifest on manifest, but the node's media type is not
   259  	// supported. Should return unsupported error
   260  	targetPlatform = ocispec.Platform{
   261  		Architecture: arc_1,
   262  		OS:           os_1,
   263  	}
   264  	root = descs[2]
   265  	_, err = SelectManifest(ctx, storage, root, &targetPlatform)
   266  	if !errors.Is(err, errdef.ErrUnsupported) {
   267  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, errdef.ErrUnsupported)
   268  	}
   269  
   270  	// generate test content without platform
   271  	storage = cas.NewMemory()
   272  	blobs = nil
   273  	descs = nil
   274  	appendBlob("test/subject", []byte("dummy subject")) // Blob 0
   275  	appendBlob(ocispec.MediaTypeImageConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
   276  	"created":"2022-07-29T08:13:55Z",
   277  	"author":"test author",
   278  	"architecture":"test-arc-1",
   279  	"os":"test-os-1",
   280  	"variant":"v1"}`)) // Blob 1
   281  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foo"))                         // Blob 2
   282  	generateManifest(arc_1, os_1, variant_1, false, &descs[0], descs[1], descs[2]) // Blob 3
   283  	generateIndex(&descs[0], descs[3])                                             // Blob 4
   284  
   285  	ctx = context.Background()
   286  	for i := range blobs {
   287  		err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
   288  		if err != nil {
   289  			t.Fatalf("failed to push test content to src: %d: %v", i, err)
   290  		}
   291  	}
   292  
   293  	// Test SelectManifest on an image index when no platform exists in the manifest list and a target platform is provided
   294  	root = descs[4]
   295  	targetPlatform = ocispec.Platform{
   296  		Architecture: arc_1,
   297  		OS:           os_1,
   298  	}
   299  	_, err = SelectManifest(ctx, storage, root, &targetPlatform)
   300  	expected = fmt.Sprintf("%s: %v: no matching manifest was found in the manifest list", root.Digest, errdef.ErrNotFound)
   301  	if err.Error() != expected {
   302  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
   303  	}
   304  
   305  	// Test SelectManifest on an image index when no platform exists in the manifest list and no target platform is provided
   306  	wantDesc = descs[3]
   307  	gotDesc, err = SelectManifest(ctx, storage, root, nil)
   308  	if err != nil {
   309  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, false)
   310  	}
   311  	if !reflect.DeepEqual(gotDesc, wantDesc) {
   312  		t.Errorf("SelectManifest() = %v, want %v", gotDesc, wantDesc)
   313  	}
   314  
   315  	// generate incorrect test content
   316  	storage = cas.NewMemory()
   317  	blobs = nil
   318  	descs = nil
   319  	appendBlob("test/subject", []byte("dummy subject")) // Blob 0
   320  	appendBlob(docker.MediaTypeConfig, []byte(`{"mediaType":"application/vnd.oci.image.config.v1+json",
   321  	"created":"2022-07-29T08:13:55Z",
   322  	"author":"test author 1",
   323  	"architecture":"test-arc-1",
   324  	"os":"test-os-1",
   325  	"variant":"v1"}`)) // Blob 1
   326  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foo1"))                       // Blob 2
   327  	generateManifest(arc_1, os_1, variant_1, true, &descs[0], descs[1], descs[2]) // Blob 3
   328  	generateIndex(&descs[0], descs[3])                                            // Blob 4
   329  
   330  	ctx = context.Background()
   331  	for i := range blobs {
   332  		err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
   333  		if err != nil {
   334  			t.Fatalf("failed to push test content to src: %d: %v", i, err)
   335  		}
   336  	}
   337  
   338  	// test SelectManifest on manifest, but the manifest is
   339  	// invalid by having docker mediaType config in the manifest and oci
   340  	// mediaType in the image config. Should return error.
   341  	root = descs[3]
   342  	targetPlatform = ocispec.Platform{
   343  		Architecture: arc_1,
   344  		OS:           os_1,
   345  	}
   346  	_, err = SelectManifest(ctx, storage, root, &targetPlatform)
   347  	expected = fmt.Sprintf("fail to recognize platform from unknown config %s: expect %s", docker.MediaTypeConfig, ocispec.MediaTypeImageConfig)
   348  	if err.Error() != expected {
   349  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
   350  	}
   351  
   352  	// generate test content with null config blob
   353  	storage = cas.NewMemory()
   354  	blobs = nil
   355  	descs = nil
   356  	appendBlob("test/subject", []byte("dummy subject"))                           // Blob 0
   357  	appendBlob(ocispec.MediaTypeImageConfig, []byte("null"))                      // Blob 1
   358  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foo2"))                       // Blob 2
   359  	generateManifest(arc_1, os_1, variant_1, true, &descs[0], descs[1], descs[2]) // Blob 3
   360  	generateIndex(nil, descs[3])                                                  // Blob 4
   361  
   362  	ctx = context.Background()
   363  	for i := range blobs {
   364  		err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
   365  		if err != nil {
   366  			t.Fatalf("failed to push test content to src: %d: %v", i, err)
   367  		}
   368  	}
   369  
   370  	// test SelectManifest on manifest with null config blob,
   371  	// should return not found error.
   372  	root = descs[3]
   373  	targetPlatform = ocispec.Platform{
   374  		Architecture: arc_1,
   375  		OS:           os_1,
   376  	}
   377  	_, err = SelectManifest(ctx, storage, root, &targetPlatform)
   378  	expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
   379  	if err.Error() != expected {
   380  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
   381  	}
   382  
   383  	// generate test content with empty config blob
   384  	storage = cas.NewMemory()
   385  	blobs = nil
   386  	descs = nil
   387  	appendBlob("test/subject", []byte("dummy subject"))                     // Blob 0
   388  	appendBlob(ocispec.MediaTypeImageConfig, []byte(""))                    // Blob 1
   389  	appendBlob(ocispec.MediaTypeImageLayer, []byte("foo3"))                 // Blob 2
   390  	generateManifest(arc_1, os_1, variant_1, true, nil, descs[1], descs[2]) // Blob 3
   391  	generateIndex(&descs[0], descs[3])                                      // Blob 4
   392  
   393  	ctx = context.Background()
   394  	for i := range blobs {
   395  		err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i]))
   396  		if err != nil {
   397  			t.Fatalf("failed to push test content to src: %d: %v", i, err)
   398  		}
   399  	}
   400  
   401  	// test SelectManifest on manifest with empty config blob
   402  	// should return not found error
   403  	root = descs[3]
   404  
   405  	targetPlatform = ocispec.Platform{
   406  		Architecture: arc_1,
   407  		OS:           os_1,
   408  	}
   409  
   410  	_, err = SelectManifest(ctx, storage, root, &targetPlatform)
   411  	expected = fmt.Sprintf("%s: %v: platform in manifest does not match target platform", root.Digest, errdef.ErrNotFound)
   412  
   413  	if err.Error() != expected {
   414  		t.Fatalf("SelectManifest() error = %v, wantErr %v", err, expected)
   415  	}
   416  }