github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/content/oci/readonlyoci_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 oci 17 18 import ( 19 "bytes" 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "os" 26 "reflect" 27 "strconv" 28 "strings" 29 "testing" 30 "testing/fstest" 31 32 "github.com/opcr-io/oras-go/v2" 33 "github.com/opcr-io/oras-go/v2/content" 34 "github.com/opcr-io/oras-go/v2/content/memory" 35 "github.com/opcr-io/oras-go/v2/internal/docker" 36 "github.com/opcr-io/oras-go/v2/registry" 37 "github.com/opencontainers/go-digest" 38 specs "github.com/opencontainers/image-spec/specs-go" 39 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 40 "golang.org/x/sync/errgroup" 41 ) 42 43 func TestReadonlyStoreInterface(t *testing.T) { 44 var store interface{} = &ReadOnlyStore{} 45 if _, ok := store.(oras.ReadOnlyGraphTarget); !ok { 46 t.Error("&ReadOnlyStore{} does not conform oras.ReadOnlyGraphTarget") 47 } 48 if _, ok := store.(registry.TagLister); !ok { 49 t.Error("&ReadOnlyStore{} does not conform registry.TagLister") 50 } 51 } 52 53 func TestReadOnlyStore(t *testing.T) { 54 // generate test content 55 var blobs [][]byte 56 var descs []ocispec.Descriptor 57 appendBlob := func(mediaType string, blob []byte) { 58 blobs = append(blobs, blob) 59 descs = append(descs, ocispec.Descriptor{ 60 MediaType: mediaType, 61 Digest: digest.FromBytes(blob), 62 Size: int64(len(blob)), 63 }) 64 } 65 generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { 66 manifest := ocispec.Manifest{ 67 MediaType: ocispec.MediaTypeImageManifest, 68 Config: config, 69 Layers: layers, 70 } 71 manifestJSON, err := json.Marshal(manifest) 72 if err != nil { 73 t.Fatal(err) 74 } 75 appendBlob(manifest.MediaType, manifestJSON) 76 } 77 generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { 78 manifest := ocispec.Artifact{ 79 MediaType: ocispec.MediaTypeArtifactManifest, 80 Subject: &subject, 81 Blobs: blobs, 82 } 83 manifestJSON, err := json.Marshal(manifest) 84 if err != nil { 85 t.Fatal(err) 86 } 87 appendBlob(manifest.MediaType, manifestJSON) 88 } 89 90 appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 91 appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1 92 generateManifest(descs[0], descs[1]) // Blob 2 93 generateArtifactManifest(descs[2]) // Blob 3 94 subjectTag := "subject" 95 96 layout := ocispec.ImageLayout{ 97 Version: ocispec.ImageLayoutVersion, 98 } 99 layoutJSON, err := json.Marshal(layout) 100 if err != nil { 101 t.Fatalf("failed to marshal OCI layout: %v", err) 102 } 103 index := ocispec.Index{ 104 Versioned: specs.Versioned{ 105 SchemaVersion: 2, // historical value 106 }, 107 Manifests: []ocispec.Descriptor{ 108 { 109 MediaType: descs[2].MediaType, 110 Size: descs[2].Size, 111 Digest: descs[2].Digest, 112 Annotations: map[string]string{ocispec.AnnotationRefName: subjectTag}, 113 }, 114 descs[3], 115 }, 116 } 117 indexJSON, err := json.Marshal(index) 118 if err != nil { 119 t.Fatalf("failed to marshal index: %v", err) 120 } 121 122 // build fs 123 fsys := fstest.MapFS{} 124 for i, desc := range descs { 125 path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/") 126 fsys[path] = &fstest.MapFile{Data: blobs[i]} 127 } 128 fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON} 129 fsys[ociImageIndexFile] = &fstest.MapFile{Data: indexJSON} 130 131 // test read-only store 132 ctx := context.Background() 133 s, err := NewFromFS(ctx, fsys) 134 if err != nil { 135 t.Fatal("NewFromFS() error =", err) 136 } 137 138 // test resolving subject by digest 139 gotDesc, err := s.Resolve(ctx, descs[2].Digest.String()) 140 if err != nil { 141 t.Error("ReadOnlyStore.Resolve() error =", err) 142 } 143 if want := descs[2]; !reflect.DeepEqual(gotDesc, want) { 144 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 145 } 146 147 // test resolving subject by tag 148 gotDesc, err = s.Resolve(ctx, subjectTag) 149 if err != nil { 150 t.Error("ReadOnlyStore.Resolve() error =", err) 151 } 152 if want := descs[2]; !content.Equal(gotDesc, want) { 153 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 154 } 155 156 // descriptor resolved by tag should have annotations 157 if gotDesc.Annotations[ocispec.AnnotationRefName] != subjectTag { 158 t.Errorf("ReadOnlyStore.Resolve() returned descriptor without annotations %v, want %v", 159 gotDesc.Annotations, 160 map[string]string{ocispec.AnnotationRefName: subjectTag}) 161 } 162 163 // test resolving artifact by digest 164 gotDesc, err = s.Resolve(ctx, descs[3].Digest.String()) 165 if err != nil { 166 t.Error("ReadOnlyStore.Resolve() error =", err) 167 } 168 if want := descs[3]; !reflect.DeepEqual(gotDesc, want) { 169 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 170 } 171 172 // test resolving blob by digest 173 gotDesc, err = s.Resolve(ctx, descs[0].Digest.String()) 174 if err != nil { 175 t.Error("ReadOnlyStore.Resolve() error =", err) 176 } 177 if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest { 178 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 179 } 180 181 // test fetching blobs 182 eg, egCtx := errgroup.WithContext(ctx) 183 for i := range blobs { 184 eg.Go(func(i int) func() error { 185 return func() error { 186 rc, err := s.Fetch(egCtx, descs[i]) 187 if err != nil { 188 return fmt.Errorf("ReadOnlyStore.Fetch(%d) error = %v", i, err) 189 } 190 got, err := io.ReadAll(rc) 191 if err != nil { 192 return fmt.Errorf("ReadOnlyStore.Fetch(%d).Read() error = %v", i, err) 193 } 194 err = rc.Close() 195 if err != nil { 196 return fmt.Errorf("ReadOnlyStore.Fetch(%d).Close() error = %v", i, err) 197 } 198 if !bytes.Equal(got, blobs[i]) { 199 return fmt.Errorf("ReadOnlyStore.Fetch(%d) = %v, want %v", i, got, blobs[i]) 200 } 201 return nil 202 } 203 }(i)) 204 } 205 if err := eg.Wait(); err != nil { 206 t.Fatal(err) 207 } 208 209 // test predecessors 210 wants := [][]ocispec.Descriptor{ 211 {descs[2]}, // blob 0 212 {descs[2]}, // blob 1 213 {descs[3]}, // blob 2, 214 {}, // blob 3 215 } 216 for i, want := range wants { 217 predecessors, err := s.Predecessors(ctx, descs[i]) 218 if err != nil { 219 t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err) 220 } 221 if !equalDescriptorSet(predecessors, want) { 222 t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want) 223 } 224 } 225 } 226 227 func TestReadOnlyStore_DirFS(t *testing.T) { 228 tempDir := t.TempDir() 229 // build an OCI layout on disk 230 s, err := New(tempDir) 231 if err != nil { 232 t.Fatal("New() error =", err) 233 } 234 235 // generate test content 236 var blobs [][]byte 237 var descs []ocispec.Descriptor 238 appendBlob := func(mediaType string, blob []byte) { 239 blobs = append(blobs, blob) 240 descs = append(descs, ocispec.Descriptor{ 241 MediaType: mediaType, 242 Digest: digest.FromBytes(blob), 243 Size: int64(len(blob)), 244 }) 245 } 246 generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { 247 manifest := ocispec.Manifest{ 248 Config: config, 249 Layers: layers, 250 } 251 manifestJSON, err := json.Marshal(manifest) 252 if err != nil { 253 t.Fatal(err) 254 } 255 appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) 256 } 257 generateIndex := func(manifests ...ocispec.Descriptor) { 258 index := ocispec.Index{ 259 Manifests: manifests, 260 } 261 indexJSON, err := json.Marshal(index) 262 if err != nil { 263 t.Fatal(err) 264 } 265 appendBlob(ocispec.MediaTypeImageIndex, indexJSON) 266 } 267 generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { 268 var manifest ocispec.Artifact 269 manifest.Subject = &subject 270 manifest.Blobs = append(manifest.Blobs, blobs...) 271 manifestJSON, err := json.Marshal(manifest) 272 if err != nil { 273 t.Fatal(err) 274 } 275 appendBlob(ocispec.MediaTypeArtifactManifest, manifestJSON) 276 } 277 278 appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 279 appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 280 appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 281 appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 282 generateManifest(descs[0], descs[1:3]...) // Blob 4 283 generateManifest(descs[0], descs[3]) // Blob 5 284 generateManifest(descs[0], descs[1:4]...) // Blob 6 285 generateIndex(descs[4:6]...) // Blob 7 286 generateIndex(descs[6]) // Blob 8 287 generateIndex() // Blob 9 288 generateIndex(descs[7:10]...) // Blob 10 289 appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11 290 generateArtifactManifest(descs[6], descs[11]) // Blob 12 291 appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13 292 generateArtifactManifest(descs[10], descs[13]) // Blob 14 293 294 ctx := context.Background() 295 eg, egCtx := errgroup.WithContext(ctx) 296 for i := range blobs { 297 eg.Go(func(i int) func() error { 298 return func() error { 299 err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i])) 300 if err != nil { 301 return fmt.Errorf("failed to push test content to src: %d: %v", i, err) 302 } 303 return nil 304 } 305 }(i)) 306 } 307 if err := eg.Wait(); err != nil { 308 t.Fatal(err) 309 } 310 311 // tag index root 312 indexRoot := descs[10] 313 tag := "latest" 314 if err := s.Tag(ctx, indexRoot, tag); err != nil { 315 t.Fatal("Tag() error =", err) 316 } 317 318 // test read-only store 319 readonlyS, err := NewFromFS(ctx, os.DirFS(tempDir)) 320 if err != nil { 321 t.Fatal("New() error =", err) 322 } 323 324 // test resolving index root by tag 325 gotDesc, err := readonlyS.Resolve(ctx, tag) 326 if err != nil { 327 t.Fatal("ReadOnlyStore: Resolve() error =", err) 328 } 329 if !content.Equal(gotDesc, indexRoot) { 330 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, indexRoot) 331 } 332 333 // test resolving index root by digest 334 gotDesc, err = readonlyS.Resolve(ctx, indexRoot.Digest.String()) 335 if err != nil { 336 t.Fatal("ReadOnlyStore: Resolve() error =", err) 337 } 338 if !reflect.DeepEqual(gotDesc, indexRoot) { 339 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, indexRoot) 340 } 341 342 // test resolving artifact manifest by digest 343 artifactRootDesc := descs[12] 344 gotDesc, err = readonlyS.Resolve(ctx, artifactRootDesc.Digest.String()) 345 if err != nil { 346 t.Fatal("ReadOnlyStore: Resolve() error =", err) 347 } 348 if !reflect.DeepEqual(gotDesc, artifactRootDesc) { 349 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, artifactRootDesc) 350 } 351 352 // test resolving blob by digest 353 gotDesc, err = readonlyS.Resolve(ctx, descs[0].Digest.String()) 354 if err != nil { 355 t.Fatal("ReadOnlyStore: Resolve() error =", err) 356 } 357 if want := descs[0]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest { 358 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 359 } 360 361 // test fetching blobs 362 for i := range blobs { 363 eg.Go(func(i int) func() error { 364 return func() error { 365 rc, err := s.Fetch(egCtx, descs[i]) 366 if err != nil { 367 return fmt.Errorf("ReadOnlyStore.Fetch(%d) error = %v", i, err) 368 } 369 got, err := io.ReadAll(rc) 370 if err != nil { 371 return fmt.Errorf("ReadOnlyStore.Fetch(%d).Read() error = %v", i, err) 372 } 373 err = rc.Close() 374 if err != nil { 375 return fmt.Errorf("ReadOnlyStore.Fetch(%d).Close() error = %v", i, err) 376 } 377 if !bytes.Equal(got, blobs[i]) { 378 return fmt.Errorf("ReadOnlyStore.Fetch(%d) = %v, want %v", i, got, blobs[i]) 379 } 380 return nil 381 } 382 }(i)) 383 } 384 if err := eg.Wait(); err != nil { 385 t.Fatal(err) 386 } 387 388 // verify predecessors 389 wants := [][]ocispec.Descriptor{ 390 descs[4:7], // Blob 0 391 {descs[4], descs[6]}, // Blob 1 392 {descs[4], descs[6]}, // Blob 2 393 {descs[5], descs[6]}, // Blob 3 394 {descs[7]}, // Blob 4 395 {descs[7]}, // Blob 5 396 {descs[8], descs[12]}, // Blob 6 397 {descs[10]}, // Blob 7 398 {descs[10]}, // Blob 8 399 {descs[10]}, // Blob 9 400 {descs[14]}, // Blob 10 401 {descs[12]}, // Blob 11 402 nil, // Blob 12, no predecessors 403 {descs[14]}, // Blob 13 404 nil, // Blob 14, no predecessors 405 } 406 for i, want := range wants { 407 predecessors, err := readonlyS.Predecessors(ctx, descs[i]) 408 if err != nil { 409 t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err) 410 } 411 if !equalDescriptorSet(predecessors, want) { 412 t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want) 413 } 414 } 415 } 416 417 /* 418 testdata/hello-world.tar contains: 419 420 blobs/ 421 sha256/ 422 2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54 // image layer 423 f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4 // image manifest 424 faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af // manifest list 425 feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412 // config 426 index.json 427 manifest.json 428 oci-layout 429 */ 430 func TestReadOnlyStore_TarFS(t *testing.T) { 431 ctx := context.Background() 432 s, err := NewFromTar(ctx, "testdata/hello-world.tar") 433 if err != nil { 434 t.Fatal("New() error =", err) 435 } 436 437 // test data in testdata/hello-world.tar 438 descs := []ocispec.Descriptor{ 439 // desc 0: config 440 { 441 MediaType: "application/vnd.docker.container.image.v1+json", 442 Size: 1469, 443 Digest: "sha256:feb5d9fea6a5e9606aa995e879d862b825965ba48de054caab5ef356dc6b3412", 444 }, 445 // desc 1: layer 446 { 447 MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", 448 Size: 2479, 449 Digest: "sha256:2db29710123e3e53a794f2694094b9b4338aa9ee5c40b930cb8063a1be392c54", 450 }, 451 // desc 2: image manifest 452 { 453 MediaType: "application/vnd.docker.distribution.manifest.v2+json", 454 Digest: "sha256:f54a58bc1aac5ea1a25d796ae155dc228b3f0e11d046ae276b39c4bf2f13d8c4", 455 Size: 525, 456 Platform: &ocispec.Platform{ 457 Architecture: "amd64", 458 OS: "linux", 459 }, 460 }, 461 // desc 3: manifest list 462 { 463 MediaType: docker.MediaTypeManifestList, 464 Size: 2561, 465 Digest: "sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af", 466 }, 467 } 468 469 // test resolving by tag 470 for _, desc := range descs { 471 gotDesc, err := s.Resolve(ctx, desc.Digest.String()) 472 if err != nil { 473 t.Fatal("ReadOnlyStore: Resolve() error =", err) 474 } 475 if want := desc; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest { 476 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 477 } 478 } 479 // test resolving by tag 480 gotDesc, err := s.Resolve(ctx, "latest") 481 if err != nil { 482 t.Fatal("ReadOnlyStore: Resolve() error =", err) 483 } 484 if want := descs[3]; gotDesc.Size != want.Size || gotDesc.Digest != want.Digest { 485 t.Errorf("ReadOnlyStore.Resolve() = %v, want %v", gotDesc, want) 486 } 487 488 // test Predecessors 489 wantPredecessors := [][]ocispec.Descriptor{ 490 {descs[2]}, // desc 0 491 {descs[2]}, // desc 1 492 {descs[3]}, // desc 2 493 {}, // desc 3 494 } 495 for i, want := range wantPredecessors { 496 predecessors, err := s.Predecessors(ctx, descs[i]) 497 if err != nil { 498 t.Errorf("ReadOnlyStore.Predecessors(%d) error = %v", i, err) 499 } 500 if !equalDescriptorSet(predecessors, want) { 501 t.Errorf("ReadOnlyStore.Predecessors(%d) = %v, want %v", i, predecessors, want) 502 } 503 } 504 } 505 506 func TestReadOnlyStore_BadIndex(t *testing.T) { 507 content := []byte("whatever") 508 fsys := fstest.MapFS{ 509 ociImageIndexFile: &fstest.MapFile{Data: content}, 510 } 511 512 ctx := context.Background() 513 _, err := NewFromFS(ctx, fsys) 514 if err == nil { 515 t.Errorf("NewFromFS() error = %v, wantErr %v", err, true) 516 } 517 } 518 519 func TestReadOnlyStore_BadLayout(t *testing.T) { 520 content := []byte("whatever") 521 fsys := fstest.MapFS{ 522 ocispec.ImageLayoutFile: &fstest.MapFile{Data: content}, 523 } 524 525 ctx := context.Background() 526 _, err := NewFromFS(ctx, fsys) 527 if err == nil { 528 t.Errorf("NewFromFS() error = %v, wantErr %v", err, true) 529 } 530 } 531 532 func TestReadOnlyStore_Copy_OCIToMemory(t *testing.T) { 533 // generate test content 534 var blobs [][]byte 535 var descs []ocispec.Descriptor 536 appendBlob := func(mediaType string, blob []byte) { 537 blobs = append(blobs, blob) 538 descs = append(descs, ocispec.Descriptor{ 539 MediaType: mediaType, 540 Digest: digest.FromBytes(blob), 541 Size: int64(len(blob)), 542 }) 543 } 544 generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { 545 manifest := ocispec.Manifest{ 546 MediaType: ocispec.MediaTypeImageManifest, 547 Config: config, 548 Layers: layers, 549 } 550 manifestJSON, err := json.Marshal(manifest) 551 if err != nil { 552 t.Fatal(err) 553 } 554 appendBlob(manifest.MediaType, manifestJSON) 555 } 556 generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { 557 manifest := ocispec.Artifact{ 558 MediaType: ocispec.MediaTypeArtifactManifest, 559 Subject: &subject, 560 Blobs: blobs, 561 } 562 manifestJSON, err := json.Marshal(manifest) 563 if err != nil { 564 t.Fatal(err) 565 } 566 appendBlob(manifest.MediaType, manifestJSON) 567 } 568 569 appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 570 appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1 571 generateManifest(descs[0], descs[1]) // Blob 2 572 generateArtifactManifest(descs[2]) // Blob 3 573 tag := "foobar" 574 root := descs[3] 575 576 layout := ocispec.ImageLayout{ 577 Version: ocispec.ImageLayoutVersion, 578 } 579 layoutJSON, err := json.Marshal(layout) 580 if err != nil { 581 t.Fatalf("failed to marshal OCI layout: %v", err) 582 } 583 index := ocispec.Index{ 584 Versioned: specs.Versioned{ 585 SchemaVersion: 2, // historical value 586 }, 587 Manifests: []ocispec.Descriptor{ 588 { 589 MediaType: descs[3].MediaType, 590 Digest: descs[3].Digest, 591 Size: descs[3].Size, 592 Annotations: map[string]string{ 593 ocispec.AnnotationRefName: tag, 594 }, 595 }, 596 }, 597 } 598 indexJSON, err := json.Marshal(index) 599 if err != nil { 600 t.Fatalf("failed to marshal index: %v", err) 601 } 602 // build fs 603 fsys := fstest.MapFS{} 604 for i, desc := range descs { 605 path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/") 606 fsys[path] = &fstest.MapFile{Data: blobs[i]} 607 } 608 fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON} 609 fsys[ociImageIndexFile] = &fstest.MapFile{Data: indexJSON} 610 611 // test read-only store 612 ctx := context.Background() 613 src, err := NewFromFS(ctx, fsys) 614 if err != nil { 615 t.Fatal("NewFromFS() error =", err) 616 } 617 618 // test copy 619 dst := memory.New() 620 gotDesc, err := oras.Copy(ctx, src, tag, dst, "", oras.DefaultCopyOptions) 621 if err != nil { 622 t.Fatalf("Copy() error = %v, wantErr %v", err, false) 623 } 624 if !content.Equal(gotDesc, root) { 625 t.Errorf("Copy() = %v, want %v", gotDesc, root) 626 } 627 628 // verify contents 629 for i, desc := range descs { 630 exists, err := dst.Exists(ctx, desc) 631 if err != nil { 632 t.Fatalf("dst.Exists(%d) error = %v", i, err) 633 } 634 if !exists { 635 t.Errorf("dst.Exists(%d) = %v, want %v", i, exists, true) 636 } 637 } 638 639 // verify tag 640 gotDesc, err = dst.Resolve(ctx, tag) 641 if err != nil { 642 t.Fatal("dst.Resolve() error =", err) 643 } 644 if !content.Equal(gotDesc, root) { 645 t.Errorf("dst.Resolve() = %v, want %v", gotDesc, root) 646 } 647 } 648 649 func TestReadOnlyStore_Tags(t *testing.T) { 650 // generate test content 651 var blobs [][]byte 652 var descs []ocispec.Descriptor 653 appendBlob := func(mediaType string, blob []byte) { 654 blobs = append(blobs, blob) 655 descs = append(descs, ocispec.Descriptor{ 656 MediaType: mediaType, 657 Digest: digest.FromBytes(blob), 658 Size: int64(len(blob)), 659 }) 660 } 661 generateManifest := func(config ocispec.Descriptor, layers ...ocispec.Descriptor) { 662 manifest := ocispec.Manifest{ 663 MediaType: ocispec.MediaTypeImageManifest, 664 Config: config, 665 Layers: layers, 666 } 667 // add annotation to make each manifest unique 668 manifest.Annotations = map[string]string{ 669 "blob_index": strconv.Itoa(len(blobs)), 670 } 671 manifestJSON, err := json.Marshal(manifest) 672 if err != nil { 673 t.Fatal(err) 674 } 675 appendBlob(manifest.MediaType, manifestJSON) 676 } 677 678 appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 679 appendBlob(ocispec.MediaTypeImageLayer, []byte("foobar")) // Blob 1 680 generateManifest(descs[0], descs[1]) // Blob 2 681 generateManifest(descs[0], descs[1]) // Blob 3 682 generateManifest(descs[0], descs[1]) // Blob 4 683 generateManifest(descs[0], descs[1]) // Blob 5 684 generateManifest(descs[0], descs[1]) // Blob 6 685 686 layout := ocispec.ImageLayout{ 687 Version: ocispec.ImageLayoutVersion, 688 } 689 layoutJSON, err := json.Marshal(layout) 690 if err != nil { 691 t.Fatalf("failed to marshal OCI layout: %v", err) 692 } 693 694 index := ocispec.Index{ 695 Versioned: specs.Versioned{ 696 SchemaVersion: 2, // historical value 697 }, 698 } 699 for _, desc := range descs[2:] { 700 index.Manifests = append(index.Manifests, ocispec.Descriptor{ 701 MediaType: desc.MediaType, 702 Size: desc.Size, 703 Digest: desc.Digest, 704 }) 705 } 706 index.Manifests[1].Annotations = map[string]string{ocispec.AnnotationRefName: "v2"} 707 index.Manifests[2].Annotations = map[string]string{ocispec.AnnotationRefName: "v3"} 708 index.Manifests[3].Annotations = map[string]string{ocispec.AnnotationRefName: "v1"} 709 index.Manifests[4].Annotations = map[string]string{ocispec.AnnotationRefName: "v4"} 710 711 indexJSON, err := json.Marshal(index) 712 if err != nil { 713 t.Fatalf("failed to marshal index: %v", err) 714 } 715 716 // build fs 717 fsys := fstest.MapFS{} 718 for i, desc := range descs { 719 path := strings.Join([]string{"blobs", desc.Digest.Algorithm().String(), desc.Digest.Encoded()}, "/") 720 fsys[path] = &fstest.MapFile{Data: blobs[i]} 721 } 722 fsys[ocispec.ImageLayoutFile] = &fstest.MapFile{Data: layoutJSON} 723 fsys[ociImageIndexFile] = &fstest.MapFile{Data: indexJSON} 724 725 // test read-only store 726 ctx := context.Background() 727 s, err := NewFromFS(ctx, fsys) 728 if err != nil { 729 t.Fatal("NewFromFS() error =", err) 730 } 731 732 // test tags 733 tests := []struct { 734 name string 735 last string 736 want []string 737 }{ 738 { 739 name: "list all tags", 740 want: []string{"v1", "v2", "v3", "v4"}, 741 }, 742 { 743 name: "list from middle", 744 last: "v2", 745 want: []string{"v3", "v4"}, 746 }, 747 { 748 name: "list from end", 749 last: "v4", 750 want: nil, 751 }, 752 } 753 for _, tt := range tests { 754 t.Run(tt.name, func(t *testing.T) { 755 if err := s.Tags(ctx, tt.last, func(got []string) error { 756 if !reflect.DeepEqual(got, tt.want) { 757 t.Errorf("ReadOnlyStore.Tags() = %v, want %v", got, tt.want) 758 } 759 return nil 760 }); err != nil { 761 t.Errorf("ReadOnlyStore.Tags() error = %v", err) 762 } 763 }) 764 } 765 766 wantErr := errors.New("expected error") 767 if err := s.Tags(ctx, "", func(got []string) error { 768 return wantErr 769 }); err != wantErr { 770 t.Errorf("ReadOnlyStore.Tags() error = %v, wantErr %v", err, wantErr) 771 } 772 } 773 774 func Test_deleteAnnotationRefName(t *testing.T) { 775 tests := []struct { 776 name string 777 desc ocispec.Descriptor 778 want ocispec.Descriptor 779 }{ 780 { 781 name: "No annotation", 782 desc: ocispec.Descriptor{}, 783 want: ocispec.Descriptor{}, 784 }, 785 { 786 name: "Nil annotation", 787 desc: ocispec.Descriptor{Annotations: nil}, 788 want: ocispec.Descriptor{}, 789 }, 790 { 791 name: "Empty annotation", 792 desc: ocispec.Descriptor{Annotations: map[string]string{}}, 793 want: ocispec.Descriptor{Annotations: map[string]string{}}, 794 }, 795 { 796 name: "No RefName", 797 desc: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}}, 798 want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}}, 799 }, 800 { 801 name: "Empty RefName", 802 desc: ocispec.Descriptor{Annotations: map[string]string{ 803 "foo": "bar", 804 ocispec.AnnotationRefName: "", 805 }}, 806 want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}}, 807 }, 808 { 809 name: "RefName only", 810 desc: ocispec.Descriptor{Annotations: map[string]string{ocispec.AnnotationRefName: "foobar"}}, 811 want: ocispec.Descriptor{}, 812 }, 813 { 814 name: "Multiple annotations with RefName", 815 desc: ocispec.Descriptor{Annotations: map[string]string{ 816 "foo": "bar", 817 ocispec.AnnotationRefName: "foobar", 818 }}, 819 want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}}, 820 }, 821 { 822 name: "Multiple annotations with empty RefName", 823 desc: ocispec.Descriptor{Annotations: map[string]string{ 824 "foo": "bar", 825 ocispec.AnnotationRefName: "", 826 }}, 827 want: ocispec.Descriptor{Annotations: map[string]string{"foo": "bar"}}, 828 }, 829 } 830 for _, tt := range tests { 831 t.Run(tt.name, func(t *testing.T) { 832 if got := deleteAnnotationRefName(tt.desc); !reflect.DeepEqual(got, tt.want) { 833 t.Errorf("deleteAnnotationRefName() = %v, want %v", got, tt.want) 834 } 835 }) 836 } 837 }