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
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
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: `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: `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: "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 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: "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: "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
364
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: "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 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: "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: "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
474
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: "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 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: "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
571
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
577
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 }
593
View as plain text