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 }