1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package mutate_test
16
17 import (
18 "archive/tar"
19 "bytes"
20 "errors"
21 "io"
22 "os"
23 "path/filepath"
24 "reflect"
25 "strings"
26 "testing"
27 "time"
28
29 "github.com/google/go-cmp/cmp"
30 "github.com/google/go-cmp/cmp/cmpopts"
31 v1 "github.com/google/go-containerregistry/pkg/v1"
32 "github.com/google/go-containerregistry/pkg/v1/empty"
33 "github.com/google/go-containerregistry/pkg/v1/match"
34 "github.com/google/go-containerregistry/pkg/v1/mutate"
35 "github.com/google/go-containerregistry/pkg/v1/partial"
36 "github.com/google/go-containerregistry/pkg/v1/random"
37 "github.com/google/go-containerregistry/pkg/v1/stream"
38 "github.com/google/go-containerregistry/pkg/v1/tarball"
39 "github.com/google/go-containerregistry/pkg/v1/types"
40 "github.com/google/go-containerregistry/pkg/v1/validate"
41 )
42
43 func TestExtractWhiteout(t *testing.T) {
44 img, err := tarball.ImageFromPath("testdata/whiteout_image.tar", nil)
45 if err != nil {
46 t.Errorf("Error loading image: %v", err)
47 }
48 tarPath, _ := filepath.Abs("img.tar")
49 defer os.Remove(tarPath)
50 tr := tar.NewReader(mutate.Extract(img))
51 for {
52 header, err := tr.Next()
53 if errors.Is(err, io.EOF) {
54 break
55 }
56 name := header.Name
57 for _, part := range filepath.SplitList(name) {
58 if part == "foo" {
59 t.Errorf("whiteout file found in tar: %v", name)
60 }
61 }
62 }
63 }
64
65 func TestExtractOverwrittenFile(t *testing.T) {
66 img, err := tarball.ImageFromPath("testdata/overwritten_file.tar", nil)
67 if err != nil {
68 t.Fatalf("Error loading image: %v", err)
69 }
70 tr := tar.NewReader(mutate.Extract(img))
71 for {
72 header, err := tr.Next()
73 if errors.Is(err, io.EOF) {
74 break
75 }
76 name := header.Name
77 if strings.Contains(name, "foo.txt") {
78 var buf bytes.Buffer
79 buf.ReadFrom(tr)
80 if strings.Contains(buf.String(), "foo") {
81 t.Errorf("Contents of file were not correctly overwritten")
82 }
83 }
84 }
85 }
86
87
88 func TestExtractError(t *testing.T) {
89 rc := mutate.Extract(invalidImage{})
90 if _, err := io.Copy(io.Discard, rc); err == nil {
91 t.Errorf("rc.Read; got nil error")
92 } else if !strings.Contains(err.Error(), errInvalidImage.Error()) {
93 t.Errorf("rc.Read; got %v, want %v", err, errInvalidImage)
94 }
95 }
96
97
98
99 func TestExtractPartialRead(t *testing.T) {
100 rc := mutate.Extract(invalidImage{})
101 if _, err := io.Copy(io.Discard, io.LimitReader(rc, 1)); err != nil {
102 t.Errorf("Could not read one byte from reader")
103 }
104 if err := rc.Close(); err != nil {
105 t.Errorf("rc.Close: %v", err)
106 }
107 }
108
109
110 type invalidImage struct {
111 v1.Image
112 }
113
114 var errInvalidImage = errors.New("invalid image")
115
116 func (invalidImage) Layers() ([]v1.Layer, error) {
117 return nil, errInvalidImage
118 }
119
120 func TestNoopCondition(t *testing.T) {
121 source := sourceImage(t)
122
123 result, err := mutate.AppendLayers(source, []v1.Layer{}...)
124 if err != nil {
125 t.Fatalf("Unexpected error creating a writable image: %v", err)
126 }
127
128 if !manifestsAreEqual(t, source, result) {
129 t.Error("manifests are not the same")
130 }
131
132 if !configFilesAreEqual(t, source, result) {
133 t.Fatal("config files are not the same")
134 }
135 }
136
137 func TestAppendWithAddendum(t *testing.T) {
138 source := sourceImage(t)
139
140 addendum := mutate.Addendum{
141 Layer: mockLayer{},
142 History: v1.History{
143 Author: "dave",
144 },
145 URLs: []string{
146 "example.com",
147 },
148 Annotations: map[string]string{
149 "foo": "bar",
150 },
151 MediaType: types.MediaType("foo"),
152 }
153
154 result, err := mutate.Append(source, addendum)
155 if err != nil {
156 t.Fatalf("failed to append: %v", err)
157 }
158
159 layers := getLayers(t, result)
160
161 if diff := cmp.Diff(layers[1], mockLayer{}); diff != "" {
162 t.Fatalf("correct layer was not appended (-got, +want) %v", diff)
163 }
164
165 if configSizesAreEqual(t, source, result) {
166 t.Fatal("adding a layer MUST change the config file size")
167 }
168
169 cf := getConfigFile(t, result)
170
171 if diff := cmp.Diff(cf.History[1], addendum.History); diff != "" {
172 t.Fatalf("the appended history is not the same (-got, +want) %s", diff)
173 }
174
175 m, err := result.Manifest()
176 if err != nil {
177 t.Fatalf("failed to get manifest: %v", err)
178 }
179
180 if diff := cmp.Diff(m.Layers[1].URLs, addendum.URLs); diff != "" {
181 t.Fatalf("the appended URLs is not the same (-got, +want) %s", diff)
182 }
183
184 if diff := cmp.Diff(m.Layers[1].Annotations, addendum.Annotations); diff != "" {
185 t.Fatalf("the appended Annotations is not the same (-got, +want) %s", diff)
186 }
187 if diff := cmp.Diff(m.Layers[1].MediaType, addendum.MediaType); diff != "" {
188 t.Fatalf("the appended MediaType is not the same (-got, +want) %s", diff)
189 }
190 }
191
192 func TestAppendLayers(t *testing.T) {
193 source := sourceImage(t)
194 layer, err := random.Layer(100, types.DockerLayer)
195 if err != nil {
196 t.Fatal(err)
197 }
198 result, err := mutate.AppendLayers(source, layer)
199 if err != nil {
200 t.Fatalf("failed to append a layer: %v", err)
201 }
202
203 if manifestsAreEqual(t, source, result) {
204 t.Fatal("appending a layer did not mutate the manifest")
205 }
206
207 if configFilesAreEqual(t, source, result) {
208 t.Fatal("appending a layer did not mutate the config file")
209 }
210
211 if configSizesAreEqual(t, source, result) {
212 t.Fatal("adding a layer MUST change the config file size")
213 }
214
215 layers := getLayers(t, result)
216
217 if got, want := len(layers), 2; got != want {
218 t.Fatalf("Layers did not return the appended layer "+
219 "- got size %d; expected 2", len(layers))
220 }
221
222 if layers[1] != layer {
223 t.Errorf("correct layer was not appended: got %v; want %v", layers[1], layer)
224 }
225
226 if err := validate.Image(result); err != nil {
227 t.Errorf("validate.Image() = %v", err)
228 }
229 }
230
231 func TestMutateConfig(t *testing.T) {
232 source := sourceImage(t)
233 cfg, err := source.ConfigFile()
234 if err != nil {
235 t.Fatalf("error getting source config file")
236 }
237
238 newEnv := []string{"foo=bar"}
239 cfg.Config.Env = newEnv
240 result, err := mutate.Config(source, cfg.Config)
241 if err != nil {
242 t.Fatalf("failed to mutate a config: %v", err)
243 }
244
245 if manifestsAreEqual(t, source, result) {
246 t.Error("mutating the config MUST mutate the manifest")
247 }
248
249 if configFilesAreEqual(t, source, result) {
250 t.Error("mutating the config did not mutate the config file")
251 }
252
253 if configSizesAreEqual(t, source, result) {
254 t.Error("adding an environment variable MUST change the config file size")
255 }
256
257 if configDigestsAreEqual(t, source, result) {
258 t.Errorf("mutating the config MUST mutate the config digest")
259 }
260
261 if !reflect.DeepEqual(cfg.Config.Env, newEnv) {
262 t.Errorf("incorrect environment set %v!=%v", cfg.Config.Env, newEnv)
263 }
264
265 if err := validate.Image(result); err != nil {
266 t.Errorf("validate.Image() = %v", err)
267 }
268 }
269
270 type arbitrary struct {
271 }
272
273 func (arbitrary) RawManifest() ([]byte, error) {
274 return []byte(`{"hello":"world"}`), nil
275 }
276 func TestAnnotations(t *testing.T) {
277 anns := map[string]string{
278 "foo": "bar",
279 }
280
281 for _, c := range []struct {
282 desc string
283 in partial.WithRawManifest
284 want string
285 }{{
286 desc: "image",
287 in: empty.Image,
288 want: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":115,"digest":"sha256:5b943e2b943f6c81dbbd4e2eca5121f4fcc39139e3d1219d6d89bd925b77d9fe"},"layers":[],"annotations":{"foo":"bar"}}`,
289 }, {
290 desc: "index",
291 in: empty.Index,
292 want: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[],"annotations":{"foo":"bar"}}`,
293 }, {
294 desc: "arbitrary",
295 in: arbitrary{},
296 want: `{"annotations":{"foo":"bar"},"hello":"world"}`,
297 }} {
298 t.Run(c.desc, func(t *testing.T) {
299 got, err := mutate.Annotations(c.in, anns).RawManifest()
300 if err != nil {
301 t.Fatalf("Annotations: %v", err)
302 }
303 if d := cmp.Diff(c.want, string(got)); d != "" {
304 t.Errorf("Diff(-want,+got): %s", d)
305 }
306 })
307 }
308 }
309
310 func TestMutateCreatedAt(t *testing.T) {
311 source := sourceImage(t)
312 want := time.Now().Add(-2 * time.Minute)
313 result, err := mutate.CreatedAt(source, v1.Time{Time: want})
314 if err != nil {
315 t.Fatalf("CreatedAt: %v", err)
316 }
317
318 if configDigestsAreEqual(t, source, result) {
319 t.Errorf("mutating the created time MUST mutate the config digest")
320 }
321
322 got := getConfigFile(t, result).Created.Time
323 if got != want {
324 t.Errorf("mutating the created time MUST mutate the time from %v to %v", got, want)
325 }
326 }
327
328 func TestMutateTime(t *testing.T) {
329 for _, tc := range []struct {
330 name string
331 source v1.Image
332 }{
333 {
334 name: "image with matching history and layers",
335 source: sourceImage(t),
336 },
337 {
338 name: "image with empty_layer history entries",
339 source: sourceImagePath(t, "testdata/source_image_with_empty_layer_history.tar"),
340 },
341 } {
342 t.Run(tc.name, func(t *testing.T) {
343 want := time.Time{}
344 result, err := mutate.Time(tc.source, want)
345 if err != nil {
346 t.Fatalf("failed to mutate a config: %v", err)
347 }
348
349 if configDigestsAreEqual(t, tc.source, result) {
350 t.Fatal("mutating the created time MUST mutate the config digest")
351 }
352
353 mutatedOriginalConfig := getConfigFile(t, tc.source).DeepCopy()
354 gotConfig := getConfigFile(t, result)
355
356
357 mutatedOriginalConfig.Author = ""
358 mutatedOriginalConfig.Created = v1.Time{Time: want}
359 for i := range mutatedOriginalConfig.History {
360 mutatedOriginalConfig.History[i].Created = v1.Time{Time: want}
361 mutatedOriginalConfig.History[i].Author = ""
362 }
363
364 if diff := cmp.Diff(mutatedOriginalConfig, gotConfig,
365 cmpopts.IgnoreFields(v1.RootFS{}, "DiffIDs"),
366 ); diff != "" {
367 t.Errorf("configFile() mismatch (-want +got):\n%s", diff)
368 }
369 })
370 }
371 }
372
373 func TestMutateMediaType(t *testing.T) {
374 want := types.OCIManifestSchema1
375 wantCfg := types.OCIConfigJSON
376 img := mutate.MediaType(empty.Image, want)
377 img = mutate.ConfigMediaType(img, wantCfg)
378 got, err := img.MediaType()
379 if err != nil {
380 t.Fatal(err)
381 }
382 if want != got {
383 t.Errorf("%q != %q", want, got)
384 }
385 manifest, err := img.Manifest()
386 if err != nil {
387 t.Fatal(err)
388 }
389 if manifest.MediaType == "" {
390 t.Error("MediaType should be set for OCI media types")
391 }
392 if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg {
393 t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg)
394 }
395
396 want = types.DockerManifestSchema2
397 wantCfg = types.DockerConfigJSON
398 img = mutate.MediaType(img, want)
399 img = mutate.ConfigMediaType(img, wantCfg)
400 got, err = img.MediaType()
401 if err != nil {
402 t.Fatal(err)
403 }
404 if want != got {
405 t.Errorf("%q != %q", want, got)
406 }
407 manifest, err = img.Manifest()
408 if err != nil {
409 t.Fatal(err)
410 }
411 if manifest.MediaType != want {
412 t.Errorf("MediaType should be set for Docker media types: %v", manifest.MediaType)
413 }
414 if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg {
415 t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg)
416 }
417
418 want = types.OCIImageIndex
419 idx := mutate.IndexMediaType(empty.Index, want)
420 got, err = idx.MediaType()
421 if err != nil {
422 t.Fatal(err)
423 }
424 if want != got {
425 t.Errorf("%q != %q", want, got)
426 }
427 im, err := idx.IndexManifest()
428 if err != nil {
429 t.Fatal(err)
430 }
431 if im.MediaType == "" {
432 t.Error("MediaType should be set for OCI media types")
433 }
434
435 want = types.DockerManifestList
436 idx = mutate.IndexMediaType(idx, want)
437 got, err = idx.MediaType()
438 if err != nil {
439 t.Fatal(err)
440 }
441 if want != got {
442 t.Errorf("%q != %q", want, got)
443 }
444 im, err = idx.IndexManifest()
445 if err != nil {
446 t.Fatal(err)
447 }
448 if im.MediaType != want {
449 t.Errorf("MediaType should be set for Docker media types: %v", im.MediaType)
450 }
451 }
452
453 func TestAppendStreamableLayer(t *testing.T) {
454 img, err := mutate.AppendLayers(
455 sourceImage(t),
456 stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("a", 100)))),
457 stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("b", 100)))),
458 stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("c", 100)))),
459 )
460 if err != nil {
461 t.Fatalf("AppendLayers: %v", err)
462 }
463
464
465 if _, err := img.Manifest(); !errors.Is(err, stream.ErrNotComputed) {
466 t.Errorf("Manifest: got %v, want %v", err, stream.ErrNotComputed)
467 }
468
469
470 ls, err := img.Layers()
471 if err != nil {
472 t.Errorf("Layers: %v", err)
473 }
474 wantDigests := []string{
475 "sha256:bfa1c600931132f55789459e2f5a5eb85659ac91bc5a54ce09e3ed14809f8a7f",
476 "sha256:77a52b9a141dcc4d3d277d053193765dca725626f50eaf56b903ac2439cf7fd1",
477 "sha256:b78472d63f6e3d31059819173b56fcb0d9479a2b13c097d4addd84889f6aff06",
478 }
479 for i, l := range ls[1:] {
480 rc, err := l.Compressed()
481 if err != nil {
482 t.Errorf("Layer %d Compressed: %v", i, err)
483 }
484
485
486
487 if _, err := io.Copy(io.Discard, rc); err != nil {
488 t.Errorf("Reading layer %d: %v", i, err)
489 }
490 if err := rc.Close(); err != nil {
491 t.Errorf("Closing layer %d: %v", i, err)
492 }
493
494
495 h, err := l.Digest()
496 if err != nil {
497 t.Errorf("Digest after consuming layer %d: %v", i, err)
498 }
499 if h.String() != wantDigests[i] {
500 t.Errorf("Layer %d digest got %q, want %q", i, h, wantDigests[i])
501 }
502 }
503
504
505
506 if _, err := img.Manifest(); err != nil {
507 t.Errorf("Manifest: %v", err)
508 }
509
510 h, err := img.Digest()
511 if err != nil {
512 t.Errorf("Digest: %v", err)
513 }
514 wantDigest := "sha256:14d140947afedc6901b490265a08bc8ebe7f9d9faed6fdf19a451f054a7dd746"
515 if h.String() != wantDigest {
516 t.Errorf("Image digest got %q, want %q", h, wantDigest)
517 }
518 }
519
520 func TestCanonical(t *testing.T) {
521 source := sourceImage(t)
522 img, err := mutate.Canonical(source)
523 if err != nil {
524 t.Fatal(err)
525 }
526 sourceCf, err := source.ConfigFile()
527 if err != nil {
528 t.Fatal(err)
529 }
530 cf, err := img.ConfigFile()
531 if err != nil {
532 t.Fatal(err)
533 }
534 for _, h := range cf.History {
535 want := "bazel build ..."
536 got := h.CreatedBy
537 if want != got {
538 t.Errorf("%q != %q", want, got)
539 }
540 }
541 var want, got string
542 want = cf.Architecture
543 got = sourceCf.Architecture
544 if want != got {
545 t.Errorf("%q != %q", want, got)
546 }
547 want = cf.OS
548 got = sourceCf.OS
549 if want != got {
550 t.Errorf("%q != %q", want, got)
551 }
552 want = cf.OSVersion
553 got = sourceCf.OSVersion
554 if want != got {
555 t.Errorf("%q != %q", want, got)
556 }
557 for _, s := range []string{
558 cf.Container,
559 cf.Config.Hostname,
560 cf.DockerVersion,
561 } {
562 if s != "" {
563 t.Errorf("non-zeroed string: %v", s)
564 }
565 }
566
567 expectedLayerTime := time.Unix(0, 0)
568 layers := getLayers(t, img)
569 for _, layer := range layers {
570 assertMTime(t, layer, expectedLayerTime)
571 }
572 }
573
574 func TestRemoveManifests(t *testing.T) {
575
576 count := 3
577 for i := 0; i < count; i++ {
578 ii, err := random.Index(1024, int64(count), int64(count))
579 if err != nil {
580 t.Fatal(err)
581 }
582
583 manifest, err := ii.IndexManifest()
584 if err != nil {
585 t.Fatal(err)
586 }
587 if len(manifest.Manifests) != count {
588 t.Fatalf("mismatched manifests on setup, had %d, expected %d", len(manifest.Manifests), count)
589 }
590 digest := manifest.Manifests[i].Digest
591 ii = mutate.RemoveManifests(ii, match.Digests(digest))
592 manifest, err = ii.IndexManifest()
593 if err != nil {
594 t.Fatal(err)
595 }
596 if len(manifest.Manifests) != (count - 1) {
597 t.Fatalf("mismatched manifests after removal, had %d, expected %d", len(manifest.Manifests), count-1)
598 }
599 for j, m := range manifest.Manifests {
600 if m.Digest == digest {
601 t.Fatalf("unexpectedly found removed hash %v at position %d", digest, j)
602 }
603 }
604 }
605 }
606
607 func TestImageImmutability(t *testing.T) {
608 img := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
609
610 t.Run("manifest", func(t *testing.T) {
611
612 changed, err := img.Manifest()
613 if err != nil {
614 t.Errorf("Manifest() = %v", err)
615 }
616 want := changed.DeepCopy()
617 changed.MediaType = types.DockerManifestList
618
619 if got, err := img.Manifest(); err != nil {
620 t.Errorf("Manifest() = %v", err)
621 } else if !cmp.Equal(got, want) {
622 t.Errorf("manifest changed! %s", cmp.Diff(got, want))
623 }
624 })
625
626 t.Run("config file", func(t *testing.T) {
627
628 changed, err := img.ConfigFile()
629 if err != nil {
630 t.Errorf("ConfigFile() = %v", err)
631 }
632 want := changed.DeepCopy()
633 changed.Author = "Jay Pegg"
634
635 if got, err := img.ConfigFile(); err != nil {
636 t.Errorf("ConfigFile() = %v", err)
637 } else if !cmp.Equal(got, want) {
638 t.Errorf("ConfigFile changed! %s", cmp.Diff(got, want))
639 }
640 })
641 }
642
643 func assertMTime(t *testing.T, layer v1.Layer, expectedTime time.Time) {
644 l, err := layer.Uncompressed()
645
646 if err != nil {
647 t.Fatalf("reading layer failed: %v", err)
648 }
649
650 tr := tar.NewReader(l)
651 for {
652 header, err := tr.Next()
653 if errors.Is(err, io.EOF) {
654 break
655 }
656 if err != nil {
657 t.Fatalf("Error reading layer: %v", err)
658 }
659
660 mtime := header.ModTime
661 if mtime.Equal(expectedTime) == false {
662 t.Errorf("unexpected mod time for layer. expected %v, got %v.", expectedTime, mtime)
663 }
664 }
665 }
666
667 func sourceImage(t *testing.T) v1.Image {
668 return sourceImagePath(t, "testdata/source_image.tar")
669 }
670
671 func sourceImagePath(t *testing.T, tarPath string) v1.Image {
672 t.Helper()
673
674 image, err := tarball.ImageFromPath(tarPath, nil)
675 if err != nil {
676 t.Fatalf("Error loading image: %v", err)
677 }
678 return image
679 }
680
681 func getManifest(t *testing.T, i v1.Image) *v1.Manifest {
682 t.Helper()
683
684 m, err := i.Manifest()
685 if err != nil {
686 t.Fatalf("Error fetching image manifest: %v", err)
687 }
688
689 return m
690 }
691
692 func getLayers(t *testing.T, i v1.Image) []v1.Layer {
693 t.Helper()
694
695 l, err := i.Layers()
696 if err != nil {
697 t.Fatalf("Error fetching image layers: %v", err)
698 }
699
700 return l
701 }
702
703 func getConfigFile(t *testing.T, i v1.Image) *v1.ConfigFile {
704 t.Helper()
705
706 c, err := i.ConfigFile()
707 if err != nil {
708 t.Fatalf("Error fetching image config file: %v", err)
709 }
710
711 return c
712 }
713
714 func configFilesAreEqual(t *testing.T, first, second v1.Image) bool {
715 t.Helper()
716
717 fc := getConfigFile(t, first)
718 sc := getConfigFile(t, second)
719
720 return cmp.Equal(fc, sc)
721 }
722
723 func configDigestsAreEqual(t *testing.T, first, second v1.Image) bool {
724 t.Helper()
725
726 fm := getManifest(t, first)
727 sm := getManifest(t, second)
728
729 return fm.Config.Digest == sm.Config.Digest
730 }
731
732 func configSizesAreEqual(t *testing.T, first, second v1.Image) bool {
733 t.Helper()
734
735 fm := getManifest(t, first)
736 sm := getManifest(t, second)
737
738 return fm.Config.Size == sm.Config.Size
739 }
740
741 func manifestsAreEqual(t *testing.T, first, second v1.Image) bool {
742 t.Helper()
743
744 fm := getManifest(t, first)
745 sm := getManifest(t, second)
746
747 return cmp.Equal(fm, sm)
748 }
749
750 type mockLayer struct{}
751
752 func (m mockLayer) Digest() (v1.Hash, error) {
753 return v1.Hash{Algorithm: "fake", Hex: "digest"}, nil
754 }
755
756 func (m mockLayer) DiffID() (v1.Hash, error) {
757 return v1.Hash{Algorithm: "fake", Hex: "diff id"}, nil
758 }
759
760 func (m mockLayer) MediaType() (types.MediaType, error) {
761 return "some-media-type", nil
762 }
763
764 func (m mockLayer) Size() (int64, error) { return 137438691328, nil }
765 func (m mockLayer) Compressed() (io.ReadCloser, error) {
766 return io.NopCloser(strings.NewReader("compressed times")), nil
767 }
768 func (m mockLayer) Uncompressed() (io.ReadCloser, error) {
769 return io.NopCloser(strings.NewReader("uncompressed")), nil
770 }
771
View as plain text