cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ocimem/check_test.go (about) 1 package ocimem 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "testing" 8 9 "cuelabs.dev/go/oci/ociregistry" 10 "cuelabs.dev/go/oci/ociregistry/ocitest" 11 "github.com/go-quicktest/qt" 12 "github.com/opencontainers/go-digest" 13 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 14 ) 15 16 var pushManifestTests = []struct { 17 testName string 18 preload ocitest.RepoContent 19 config Config 20 tag string 21 mediaType string 22 manifestData func(content ocitest.PushedRepoContent) []byte 23 wantError string 24 }{{ 25 testName: "NonExistentConfigReference", 26 mediaType: ocispec.MediaTypeImageManifest, 27 manifestData: func(ocitest.PushedRepoContent) []byte { 28 return mustJSONMarshal(ociregistry.Manifest{ 29 MediaType: ocispec.MediaTypeImageManifest, 30 Config: ociregistry.Descriptor{ 31 MediaType: "application/something", 32 Size: 1, 33 Digest: digest.FromString("a"), 34 }, 35 }) 36 }, 37 wantError: `invalid manifest: blob for config not found`, 38 }, { 39 testName: "NonExistentLayerReference", 40 preload: ocitest.RepoContent{ 41 Blobs: map[string]string{ 42 "a": "{}", 43 }, 44 }, 45 mediaType: ocispec.MediaTypeImageManifest, 46 manifestData: func(content ocitest.PushedRepoContent) []byte { 47 return mustJSONMarshal(ociregistry.Manifest{ 48 MediaType: ocispec.MediaTypeImageManifest, 49 Config: content.Blobs["a"], 50 Layers: []ociregistry.Descriptor{{ 51 MediaType: "application/something", 52 Size: 1, 53 Digest: digest.FromString("b"), 54 }}, 55 }) 56 }, 57 wantError: `invalid manifest: blob for layers\[0\] not found`, 58 }, { 59 testName: "NonExistentSubjectReference", 60 preload: ocitest.RepoContent{ 61 Blobs: map[string]string{ 62 "a": "{}", 63 }, 64 }, 65 mediaType: ocispec.MediaTypeImageManifest, 66 manifestData: func(content ocitest.PushedRepoContent) []byte { 67 return mustJSONMarshal(ociregistry.Manifest{ 68 MediaType: ocispec.MediaTypeImageManifest, 69 Config: content.Blobs["a"], 70 Subject: &ociregistry.Descriptor{ 71 MediaType: "application/something", 72 Size: 1, 73 Digest: digest.FromString("b"), 74 }, 75 }) 76 }, 77 // Non-existent subject references are explicitly allowed. 78 }, { 79 testName: "NonExistentImageIndexManifestReference", 80 mediaType: ocispec.MediaTypeImageIndex, 81 manifestData: func(content ocitest.PushedRepoContent) []byte { 82 return mustJSONMarshal(ocispec.Index{ 83 MediaType: ocispec.MediaTypeImageIndex, 84 Manifests: []ociregistry.Descriptor{{ 85 MediaType: ocispec.MediaTypeImageManifest, 86 Size: 1, 87 Digest: digest.FromString("a"), 88 }}, 89 }) 90 }, 91 wantError: `invalid manifest: manifest for manifests\[0\] not found`, 92 }, { 93 testName: "NonExistentImageIndexSubjectReference", 94 mediaType: ocispec.MediaTypeImageIndex, 95 manifestData: func(content ocitest.PushedRepoContent) []byte { 96 return mustJSONMarshal(ocispec.Index{ 97 MediaType: ocispec.MediaTypeImageIndex, 98 Subject: &ociregistry.Descriptor{ 99 MediaType: "application/something", 100 Size: 1, 101 Digest: digest.FromString("b"), 102 }, 103 }) 104 }, 105 // Non-existent subject references are explicitly allowed. 106 }, { 107 testName: "CannotOverwriteTagWhenImmutabilityEnabled", 108 preload: ocitest.RepoContent{ 109 Blobs: map[string]string{ 110 "a": "{}", 111 "b": "other", 112 }, 113 Manifests: map[string]ociregistry.Manifest{ 114 "m": { 115 MediaType: ocispec.MediaTypeImageManifest, 116 Config: ociregistry.Descriptor{ 117 Digest: "a", 118 }, 119 Layers: []ociregistry.Descriptor{{ 120 Digest: "a", 121 }}, 122 }, 123 }, 124 Tags: map[string]string{ 125 "sometag": "m", 126 }, 127 }, 128 config: Config{ 129 ImmutableTags: true, 130 }, 131 mediaType: ocispec.MediaTypeImageManifest, 132 tag: "sometag", 133 manifestData: func(content ocitest.PushedRepoContent) []byte { 134 return mustJSONMarshal(ociregistry.Manifest{ 135 MediaType: ocispec.MediaTypeImageManifest, 136 Config: content.Blobs["a"], 137 Layers: []ociregistry.Descriptor{content.Blobs["a"]}, 138 Annotations: map[string]string{ 139 "different": "thing", 140 }, 141 }) 142 }, 143 wantError: `denied: requested access to the resource is denied: cannot overwrite tag`, 144 }, { 145 testName: "CanRewriteTagWithIdenticalContentsWhenImmutabilityEnabled", 146 preload: ocitest.RepoContent{ 147 Blobs: map[string]string{ 148 "a": "{}", 149 "b": "other", 150 }, 151 Manifests: map[string]ociregistry.Manifest{ 152 "m": { 153 MediaType: ocispec.MediaTypeImageManifest, 154 Config: ociregistry.Descriptor{ 155 Digest: "a", 156 }, 157 Layers: []ociregistry.Descriptor{{ 158 Digest: "a", 159 }}, 160 }, 161 }, 162 Tags: map[string]string{ 163 "sometag": "m", 164 }, 165 }, 166 config: Config{ 167 ImmutableTags: true, 168 }, 169 mediaType: ocispec.MediaTypeImageManifest, 170 tag: "sometag", 171 manifestData: func(content ocitest.PushedRepoContent) []byte { 172 return content.ManifestData["m"] 173 }, 174 }, { 175 testName: "CannotRewriteTagWithIdenticalContentsButDifferentMediaTypeWhenImmutabilityEnabled", 176 preload: ocitest.RepoContent{ 177 Blobs: map[string]string{ 178 "a": "{}", 179 "b": "other", 180 }, 181 Manifests: map[string]ociregistry.Manifest{ 182 "m": { 183 MediaType: ocispec.MediaTypeImageManifest, 184 Config: ociregistry.Descriptor{ 185 Digest: "a", 186 }, 187 Layers: []ociregistry.Descriptor{{ 188 Digest: "a", 189 }}, 190 }, 191 }, 192 Tags: map[string]string{ 193 "sometag": "m", 194 }, 195 }, 196 config: Config{ 197 ImmutableTags: true, 198 }, 199 mediaType: "application/vnd.docker.container.image.v1+json", 200 tag: "sometag", 201 manifestData: func(content ocitest.PushedRepoContent) []byte { 202 return content.ManifestData["m"] 203 }, 204 wantError: `denied: requested access to the resource is denied: mismatched media type`, 205 }, { 206 testName: "CanOverwriteTagWhenImmutabilityNotEnabled", 207 preload: ocitest.RepoContent{ 208 Blobs: map[string]string{ 209 "a": "{}", 210 "b": "other", 211 }, 212 Manifests: map[string]ociregistry.Manifest{ 213 "m": { 214 MediaType: ocispec.MediaTypeImageManifest, 215 Config: ociregistry.Descriptor{ 216 Digest: "a", 217 }, 218 Layers: []ociregistry.Descriptor{{ 219 Digest: "a", 220 }}, 221 }, 222 }, 223 Tags: map[string]string{ 224 "sometag": "m", 225 }, 226 }, 227 mediaType: ocispec.MediaTypeImageManifest, 228 tag: "sometag", 229 manifestData: func(content ocitest.PushedRepoContent) []byte { 230 return mustJSONMarshal(ociregistry.Manifest{ 231 MediaType: ocispec.MediaTypeImageManifest, 232 Config: content.Blobs["a"], 233 Layers: []ociregistry.Descriptor{content.Blobs["a"]}, 234 Annotations: map[string]string{ 235 "different": "thing", 236 }, 237 }) 238 }, 239 }} 240 241 func TestPushManifest(t *testing.T) { 242 for _, test := range pushManifestTests { 243 t.Run(test.testName, func(t *testing.T) { 244 ctx := context.Background() 245 r := ocitest.NewRegistry(t, NewWithConfig(&test.config)) 246 content := r.MustPushContent(ocitest.RegistryContent{ 247 "test": test.preload, 248 })["test"] 249 data := test.manifestData(content) 250 _, err := r.R.PushManifest(ctx, "test", test.tag, data, test.mediaType) 251 if test.wantError != "" { 252 qt.Assert(t, qt.ErrorMatches(err, test.wantError)) 253 } else { 254 qt.Assert(t, qt.IsNil(err)) 255 } 256 }) 257 } 258 } 259 260 var deleteBlobTests = []struct { 261 testName string 262 config Config 263 preload ocitest.RepoContent 264 getDigest func(content ocitest.PushedRepoContent) ociregistry.Digest 265 wantError string 266 }{{ 267 testName: "NonExistentRepo", 268 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 269 return digest.FromString("blshdfsvg") 270 }, 271 wantError: "name unknown: repository name not known to registry", 272 }, { 273 testName: "NonExistentBlob", 274 preload: ocitest.RepoContent{ 275 Blobs: map[string]string{ 276 "a": "{}", 277 }, 278 }, 279 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 280 return digest.FromString("blshdfsvg") 281 }, 282 wantError: "blob unknown: blob unknown to registry", 283 }, { 284 testName: "TaggedBlobWithImmutableTags", 285 config: Config{ 286 ImmutableTags: true, 287 }, 288 preload: ocitest.RepoContent{ 289 Blobs: map[string]string{ 290 "a": "{}", 291 }, 292 Manifests: map[string]ociregistry.Manifest{ 293 "m": { 294 MediaType: ocispec.MediaTypeImageManifest, 295 Config: ociregistry.Descriptor{ 296 Digest: "a", 297 }, 298 Layers: []ociregistry.Descriptor{{ 299 Digest: "a", 300 }}, 301 }, 302 }, 303 Tags: map[string]string{ 304 "sometag": "m", 305 }, 306 }, 307 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 308 return content.Blobs["a"].Digest 309 }, 310 wantError: "denied: requested access to the resource is denied: deletion of tagged blob not permitted", 311 }, { 312 testName: "IndirectlyTaggedBlobWithImmutableTags", 313 config: Config{ 314 ImmutableTags: true, 315 }, 316 preload: ocitest.RepoContent{ 317 Blobs: map[string]string{ 318 "a": "{}", 319 "b": "other", 320 }, 321 Manifests: map[string]ociregistry.Manifest{ 322 "m0": { 323 MediaType: ocispec.MediaTypeImageManifest, 324 Config: ociregistry.Descriptor{ 325 Digest: "a", 326 }, 327 }, 328 "m1": { 329 MediaType: ocispec.MediaTypeImageManifest, 330 Config: ociregistry.Descriptor{ 331 Digest: "b", 332 }, 333 Subject: &ociregistry.Descriptor{ 334 Digest: "m0", 335 }, 336 }, 337 }, 338 Tags: map[string]string{ 339 "sometag": "m1", 340 }, 341 }, 342 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 343 return content.Blobs["a"].Digest 344 }, 345 wantError: "denied: requested access to the resource is denied: deletion of tagged blob not permitted", 346 }} 347 348 func TestDeleteBlob(t *testing.T) { 349 for _, test := range deleteBlobTests { 350 t.Run(test.testName, func(t *testing.T) { 351 ctx := context.Background() 352 r := ocitest.NewRegistry(t, NewWithConfig(&test.config)) 353 content := r.MustPushContent(ocitest.RegistryContent{ 354 "test": test.preload, 355 })["test"] 356 digest := test.getDigest(content) 357 err := r.R.DeleteBlob(ctx, "test", digest) 358 if test.wantError != "" { 359 qt.Assert(t, qt.ErrorMatches(err, test.wantError)) 360 } else { 361 qt.Assert(t, qt.IsNil(err)) 362 } 363 // Regardless of the result, the blob shouldn't be there afterwards 364 // unless the operation was denied. 365 if !errors.Is(err, ociregistry.ErrDenied) { 366 _, err := r.R.ResolveBlob(ctx, "test", digest) 367 qt.Assert(t, qt.Not(qt.IsNil(err))) 368 } 369 }) 370 } 371 } 372 373 var deleteManifestTests = []struct { 374 testName string 375 config Config 376 preload ocitest.RepoContent 377 getDigest func(content ocitest.PushedRepoContent) ociregistry.Digest 378 wantError string 379 }{{ 380 testName: "NonExistentRepo", 381 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 382 return digest.FromString("blshdfsvg") 383 }, 384 wantError: "name unknown: repository name not known to registry", 385 }, { 386 testName: "NonExistentManifest", 387 preload: ocitest.RepoContent{ 388 Blobs: map[string]string{ 389 "a": "{}", 390 }, 391 }, 392 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 393 return digest.FromString("blshdfsvg") 394 }, 395 wantError: "manifest unknown: manifest unknown to registry", 396 }, { 397 testName: "TaggedManifestWithImmutableTags", 398 config: Config{ 399 ImmutableTags: true, 400 }, 401 preload: ocitest.RepoContent{ 402 Blobs: map[string]string{ 403 "a": "{}", 404 }, 405 Manifests: map[string]ociregistry.Manifest{ 406 "m": { 407 MediaType: ocispec.MediaTypeImageManifest, 408 Config: ociregistry.Descriptor{ 409 Digest: "a", 410 }, 411 }, 412 }, 413 Tags: map[string]string{ 414 "sometag": "m", 415 }, 416 }, 417 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 418 return content.Manifests["m"].Digest 419 }, 420 wantError: "denied: requested access to the resource is denied: deletion of tagged manifest not permitted", 421 }, { 422 testName: "IndirectlyTaggedManifestWithImmutableTags", 423 config: Config{ 424 ImmutableTags: true, 425 }, 426 preload: ocitest.RepoContent{ 427 Blobs: map[string]string{ 428 "a": "{}", 429 "b": "other", 430 }, 431 Manifests: map[string]ociregistry.Manifest{ 432 "m0": { 433 MediaType: ocispec.MediaTypeImageManifest, 434 Config: ociregistry.Descriptor{ 435 Digest: "a", 436 }, 437 }, 438 "m1": { 439 MediaType: ocispec.MediaTypeImageManifest, 440 Config: ociregistry.Descriptor{ 441 Digest: "b", 442 }, 443 Subject: &ociregistry.Descriptor{ 444 Digest: "m0", 445 }, 446 }, 447 }, 448 Tags: map[string]string{ 449 "sometag": "m1", 450 }, 451 }, 452 getDigest: func(content ocitest.PushedRepoContent) ociregistry.Digest { 453 return content.Manifests["m0"].Digest 454 }, 455 wantError: "denied: requested access to the resource is denied: deletion of tagged manifest not permitted", 456 }} 457 458 func TestDeleteManifest(t *testing.T) { 459 for _, test := range deleteManifestTests { 460 t.Run(test.testName, func(t *testing.T) { 461 ctx := context.Background() 462 r := ocitest.NewRegistry(t, NewWithConfig(&test.config)) 463 content := r.MustPushContent(ocitest.RegistryContent{ 464 "test": test.preload, 465 })["test"] 466 digest := test.getDigest(content) 467 err := r.R.DeleteManifest(ctx, "test", digest) 468 if test.wantError != "" { 469 qt.Assert(t, qt.ErrorMatches(err, test.wantError)) 470 } else { 471 qt.Assert(t, qt.IsNil(err)) 472 } 473 // Regardless of the result, the manifest shouldn't be there afterwards 474 // unless the operation was denied. 475 if !errors.Is(err, ociregistry.ErrDenied) { 476 _, err := r.R.ResolveManifest(ctx, "test", digest) 477 qt.Assert(t, qt.Not(qt.IsNil(err))) 478 } 479 }) 480 } 481 } 482 483 var deleteTagTests = []struct { 484 testName string 485 config Config 486 preload ocitest.RepoContent 487 tag string 488 wantError string 489 }{{ 490 testName: "NonExistentRepo", 491 tag: "foo", 492 wantError: "name unknown: repository name not known to registry", 493 }, { 494 testName: "NonExistentTag", 495 preload: ocitest.RepoContent{ 496 Blobs: map[string]string{ 497 "a": "{}", 498 }, 499 }, 500 tag: "foo", 501 wantError: "manifest unknown: manifest unknown to registry: tag does not exist", 502 }, { 503 testName: "WithImmutableTags", 504 config: Config{ 505 ImmutableTags: true, 506 }, 507 preload: ocitest.RepoContent{ 508 Blobs: map[string]string{ 509 "a": "{}", 510 }, 511 Manifests: map[string]ociregistry.Manifest{ 512 "m": { 513 MediaType: ocispec.MediaTypeImageManifest, 514 Config: ociregistry.Descriptor{ 515 Digest: "a", 516 }, 517 }, 518 }, 519 Tags: map[string]string{ 520 "sometag": "m", 521 }, 522 }, 523 tag: "sometag", 524 wantError: "denied: requested access to the resource is denied: tag deletion not permitted", 525 }, { 526 testName: "Success", 527 preload: ocitest.RepoContent{ 528 Blobs: map[string]string{ 529 "a": "{}", 530 "b": "other", 531 }, 532 Manifests: map[string]ociregistry.Manifest{ 533 "m0": { 534 MediaType: ocispec.MediaTypeImageManifest, 535 Config: ociregistry.Descriptor{ 536 Digest: "a", 537 }, 538 }, 539 "m1": { 540 MediaType: ocispec.MediaTypeImageManifest, 541 Config: ociregistry.Descriptor{ 542 Digest: "b", 543 }, 544 Subject: &ociregistry.Descriptor{ 545 Digest: "m0", 546 }, 547 }, 548 }, 549 Tags: map[string]string{ 550 "sometag": "m1", 551 }, 552 }, 553 tag: "sometag", 554 }} 555 556 func TestDeleteTag(t *testing.T) { 557 for _, test := range deleteTagTests { 558 t.Run(test.testName, func(t *testing.T) { 559 ctx := context.Background() 560 r := ocitest.NewRegistry(t, NewWithConfig(&test.config)) 561 content := r.MustPushContent(ocitest.RegistryContent{ 562 "test": test.preload, 563 })["test"] 564 err := r.R.DeleteTag(ctx, "test", test.tag) 565 if test.wantError != "" { 566 qt.Assert(t, qt.ErrorMatches(err, test.wantError)) 567 } else { 568 qt.Assert(t, qt.IsNil(err)) 569 } 570 // Regardless of the result, the tag shouldn't be there afterwards 571 // unless the operation was denied. 572 if !errors.Is(err, ociregistry.ErrDenied) { 573 _, err := r.R.ResolveTag(ctx, "test", test.tag) 574 qt.Assert(t, qt.Not(qt.IsNil(err))) 575 } 576 // The manifest should remain present even though the tag 577 // itself has been deleted. 578 if tagDesc, ok := content.Manifests[test.preload.Tags[test.tag]]; ok { 579 _, err := r.R.ResolveManifest(ctx, "test", tagDesc.Digest) 580 qt.Assert(t, qt.IsNil(err)) 581 } 582 }) 583 } 584 } 585 586 func mustJSONMarshal(x any) []byte { 587 data, err := json.Marshal(x) 588 if err != nil { 589 panic(err) 590 } 591 return data 592 }