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