// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package mutate_test import ( "archive/tar" "bytes" "errors" "io" "os" "path/filepath" "reflect" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/match" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/stream" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/google/go-containerregistry/pkg/v1/validate" ) func TestExtractWhiteout(t *testing.T) { img, err := tarball.ImageFromPath("testdata/whiteout_image.tar", nil) if err != nil { t.Errorf("Error loading image: %v", err) } tarPath, _ := filepath.Abs("img.tar") defer os.Remove(tarPath) tr := tar.NewReader(mutate.Extract(img)) for { header, err := tr.Next() if errors.Is(err, io.EOF) { break } name := header.Name for _, part := range filepath.SplitList(name) { if part == "foo" { t.Errorf("whiteout file found in tar: %v", name) } } } } func TestExtractOverwrittenFile(t *testing.T) { img, err := tarball.ImageFromPath("testdata/overwritten_file.tar", nil) if err != nil { t.Fatalf("Error loading image: %v", err) } tr := tar.NewReader(mutate.Extract(img)) for { header, err := tr.Next() if errors.Is(err, io.EOF) { break } name := header.Name if strings.Contains(name, "foo.txt") { var buf bytes.Buffer buf.ReadFrom(tr) if strings.Contains(buf.String(), "foo") { t.Errorf("Contents of file were not correctly overwritten") } } } } // TestExtractError tests that if there are any errors encountered func TestExtractError(t *testing.T) { rc := mutate.Extract(invalidImage{}) if _, err := io.Copy(io.Discard, rc); err == nil { t.Errorf("rc.Read; got nil error") } else if !strings.Contains(err.Error(), errInvalidImage.Error()) { t.Errorf("rc.Read; got %v, want %v", err, errInvalidImage) } } // TestExtractPartialRead tests that the reader can be partially read (e.g., // tar headers) and closed without error. func TestExtractPartialRead(t *testing.T) { rc := mutate.Extract(invalidImage{}) if _, err := io.Copy(io.Discard, io.LimitReader(rc, 1)); err != nil { t.Errorf("Could not read one byte from reader") } if err := rc.Close(); err != nil { t.Errorf("rc.Close: %v", err) } } // invalidImage is an image which returns an error when Layers() is called. type invalidImage struct { v1.Image } var errInvalidImage = errors.New("invalid image") func (invalidImage) Layers() ([]v1.Layer, error) { return nil, errInvalidImage } func TestNoopCondition(t *testing.T) { source := sourceImage(t) result, err := mutate.AppendLayers(source, []v1.Layer{}...) if err != nil { t.Fatalf("Unexpected error creating a writable image: %v", err) } if !manifestsAreEqual(t, source, result) { t.Error("manifests are not the same") } if !configFilesAreEqual(t, source, result) { t.Fatal("config files are not the same") } } func TestAppendWithAddendum(t *testing.T) { source := sourceImage(t) addendum := mutate.Addendum{ Layer: mockLayer{}, History: v1.History{ Author: "dave", }, URLs: []string{ "example.com", }, Annotations: map[string]string{ "foo": "bar", }, MediaType: types.MediaType("foo"), } result, err := mutate.Append(source, addendum) if err != nil { t.Fatalf("failed to append: %v", err) } layers := getLayers(t, result) if diff := cmp.Diff(layers[1], mockLayer{}); diff != "" { t.Fatalf("correct layer was not appended (-got, +want) %v", diff) } if configSizesAreEqual(t, source, result) { t.Fatal("adding a layer MUST change the config file size") } cf := getConfigFile(t, result) if diff := cmp.Diff(cf.History[1], addendum.History); diff != "" { t.Fatalf("the appended history is not the same (-got, +want) %s", diff) } m, err := result.Manifest() if err != nil { t.Fatalf("failed to get manifest: %v", err) } if diff := cmp.Diff(m.Layers[1].URLs, addendum.URLs); diff != "" { t.Fatalf("the appended URLs is not the same (-got, +want) %s", diff) } if diff := cmp.Diff(m.Layers[1].Annotations, addendum.Annotations); diff != "" { t.Fatalf("the appended Annotations is not the same (-got, +want) %s", diff) } if diff := cmp.Diff(m.Layers[1].MediaType, addendum.MediaType); diff != "" { t.Fatalf("the appended MediaType is not the same (-got, +want) %s", diff) } } func TestAppendLayers(t *testing.T) { source := sourceImage(t) layer, err := random.Layer(100, types.DockerLayer) if err != nil { t.Fatal(err) } result, err := mutate.AppendLayers(source, layer) if err != nil { t.Fatalf("failed to append a layer: %v", err) } if manifestsAreEqual(t, source, result) { t.Fatal("appending a layer did not mutate the manifest") } if configFilesAreEqual(t, source, result) { t.Fatal("appending a layer did not mutate the config file") } if configSizesAreEqual(t, source, result) { t.Fatal("adding a layer MUST change the config file size") } layers := getLayers(t, result) if got, want := len(layers), 2; got != want { t.Fatalf("Layers did not return the appended layer "+ "- got size %d; expected 2", len(layers)) } if layers[1] != layer { t.Errorf("correct layer was not appended: got %v; want %v", layers[1], layer) } if err := validate.Image(result); err != nil { t.Errorf("validate.Image() = %v", err) } } func TestMutateConfig(t *testing.T) { source := sourceImage(t) cfg, err := source.ConfigFile() if err != nil { t.Fatalf("error getting source config file") } newEnv := []string{"foo=bar"} cfg.Config.Env = newEnv result, err := mutate.Config(source, cfg.Config) if err != nil { t.Fatalf("failed to mutate a config: %v", err) } if manifestsAreEqual(t, source, result) { t.Error("mutating the config MUST mutate the manifest") } if configFilesAreEqual(t, source, result) { t.Error("mutating the config did not mutate the config file") } if configSizesAreEqual(t, source, result) { t.Error("adding an environment variable MUST change the config file size") } if configDigestsAreEqual(t, source, result) { t.Errorf("mutating the config MUST mutate the config digest") } if !reflect.DeepEqual(cfg.Config.Env, newEnv) { t.Errorf("incorrect environment set %v!=%v", cfg.Config.Env, newEnv) } if err := validate.Image(result); err != nil { t.Errorf("validate.Image() = %v", err) } } type arbitrary struct { } func (arbitrary) RawManifest() ([]byte, error) { return []byte(`{"hello":"world"}`), nil } func TestAnnotations(t *testing.T) { anns := map[string]string{ "foo": "bar", } for _, c := range []struct { desc string in partial.WithRawManifest want string }{{ desc: "image", in: empty.Image, 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"}}`, }, { desc: "index", in: empty.Index, want: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[],"annotations":{"foo":"bar"}}`, }, { desc: "arbitrary", in: arbitrary{}, want: `{"annotations":{"foo":"bar"},"hello":"world"}`, }} { t.Run(c.desc, func(t *testing.T) { got, err := mutate.Annotations(c.in, anns).RawManifest() if err != nil { t.Fatalf("Annotations: %v", err) } if d := cmp.Diff(c.want, string(got)); d != "" { t.Errorf("Diff(-want,+got): %s", d) } }) } } func TestMutateCreatedAt(t *testing.T) { source := sourceImage(t) want := time.Now().Add(-2 * time.Minute) result, err := mutate.CreatedAt(source, v1.Time{Time: want}) if err != nil { t.Fatalf("CreatedAt: %v", err) } if configDigestsAreEqual(t, source, result) { t.Errorf("mutating the created time MUST mutate the config digest") } got := getConfigFile(t, result).Created.Time if got != want { t.Errorf("mutating the created time MUST mutate the time from %v to %v", got, want) } } func TestMutateTime(t *testing.T) { for _, tc := range []struct { name string source v1.Image }{ { name: "image with matching history and layers", source: sourceImage(t), }, { name: "image with empty_layer history entries", source: sourceImagePath(t, "testdata/source_image_with_empty_layer_history.tar"), }, } { t.Run(tc.name, func(t *testing.T) { want := time.Time{} result, err := mutate.Time(tc.source, want) if err != nil { t.Fatalf("failed to mutate a config: %v", err) } if configDigestsAreEqual(t, tc.source, result) { t.Fatal("mutating the created time MUST mutate the config digest") } mutatedOriginalConfig := getConfigFile(t, tc.source).DeepCopy() gotConfig := getConfigFile(t, result) // manually change the fields we expect to be changed by mutate.Time mutatedOriginalConfig.Author = "" mutatedOriginalConfig.Created = v1.Time{Time: want} for i := range mutatedOriginalConfig.History { mutatedOriginalConfig.History[i].Created = v1.Time{Time: want} mutatedOriginalConfig.History[i].Author = "" } if diff := cmp.Diff(mutatedOriginalConfig, gotConfig, cmpopts.IgnoreFields(v1.RootFS{}, "DiffIDs"), ); diff != "" { t.Errorf("configFile() mismatch (-want +got):\n%s", diff) } }) } } func TestMutateMediaType(t *testing.T) { want := types.OCIManifestSchema1 wantCfg := types.OCIConfigJSON img := mutate.MediaType(empty.Image, want) img = mutate.ConfigMediaType(img, wantCfg) got, err := img.MediaType() if err != nil { t.Fatal(err) } if want != got { t.Errorf("%q != %q", want, got) } manifest, err := img.Manifest() if err != nil { t.Fatal(err) } if manifest.MediaType == "" { t.Error("MediaType should be set for OCI media types") } if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg { t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg) } want = types.DockerManifestSchema2 wantCfg = types.DockerConfigJSON img = mutate.MediaType(img, want) img = mutate.ConfigMediaType(img, wantCfg) got, err = img.MediaType() if err != nil { t.Fatal(err) } if want != got { t.Errorf("%q != %q", want, got) } manifest, err = img.Manifest() if err != nil { t.Fatal(err) } if manifest.MediaType != want { t.Errorf("MediaType should be set for Docker media types: %v", manifest.MediaType) } if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg { t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg) } want = types.OCIImageIndex idx := mutate.IndexMediaType(empty.Index, want) got, err = idx.MediaType() if err != nil { t.Fatal(err) } if want != got { t.Errorf("%q != %q", want, got) } im, err := idx.IndexManifest() if err != nil { t.Fatal(err) } if im.MediaType == "" { t.Error("MediaType should be set for OCI media types") } want = types.DockerManifestList idx = mutate.IndexMediaType(idx, want) got, err = idx.MediaType() if err != nil { t.Fatal(err) } if want != got { t.Errorf("%q != %q", want, got) } im, err = idx.IndexManifest() if err != nil { t.Fatal(err) } if im.MediaType != want { t.Errorf("MediaType should be set for Docker media types: %v", im.MediaType) } } func TestAppendStreamableLayer(t *testing.T) { img, err := mutate.AppendLayers( sourceImage(t), stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("a", 100)))), stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("b", 100)))), stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("c", 100)))), ) if err != nil { t.Fatalf("AppendLayers: %v", err) } // Until the streams are consumed, the image manifest is not yet computed. if _, err := img.Manifest(); !errors.Is(err, stream.ErrNotComputed) { t.Errorf("Manifest: got %v, want %v", err, stream.ErrNotComputed) } // We can still get Layers while some are not yet computed. ls, err := img.Layers() if err != nil { t.Errorf("Layers: %v", err) } wantDigests := []string{ "sha256:bfa1c600931132f55789459e2f5a5eb85659ac91bc5a54ce09e3ed14809f8a7f", "sha256:77a52b9a141dcc4d3d277d053193765dca725626f50eaf56b903ac2439cf7fd1", "sha256:b78472d63f6e3d31059819173b56fcb0d9479a2b13c097d4addd84889f6aff06", } for i, l := range ls[1:] { rc, err := l.Compressed() if err != nil { t.Errorf("Layer %d Compressed: %v", i, err) } // Consume the layer's stream and close it to compute the // layer's metadata. if _, err := io.Copy(io.Discard, rc); err != nil { t.Errorf("Reading layer %d: %v", i, err) } if err := rc.Close(); err != nil { t.Errorf("Closing layer %d: %v", i, err) } // The layer's metadata is now available. h, err := l.Digest() if err != nil { t.Errorf("Digest after consuming layer %d: %v", i, err) } if h.String() != wantDigests[i] { t.Errorf("Layer %d digest got %q, want %q", i, h, wantDigests[i]) } } // Now that the streamable layers have been consumed, the image's // manifest can be computed. if _, err := img.Manifest(); err != nil { t.Errorf("Manifest: %v", err) } h, err := img.Digest() if err != nil { t.Errorf("Digest: %v", err) } wantDigest := "sha256:14d140947afedc6901b490265a08bc8ebe7f9d9faed6fdf19a451f054a7dd746" if h.String() != wantDigest { t.Errorf("Image digest got %q, want %q", h, wantDigest) } } func TestCanonical(t *testing.T) { source := sourceImage(t) img, err := mutate.Canonical(source) if err != nil { t.Fatal(err) } sourceCf, err := source.ConfigFile() if err != nil { t.Fatal(err) } cf, err := img.ConfigFile() if err != nil { t.Fatal(err) } for _, h := range cf.History { want := "bazel build ..." got := h.CreatedBy if want != got { t.Errorf("%q != %q", want, got) } } var want, got string want = cf.Architecture got = sourceCf.Architecture if want != got { t.Errorf("%q != %q", want, got) } want = cf.OS got = sourceCf.OS if want != got { t.Errorf("%q != %q", want, got) } want = cf.OSVersion got = sourceCf.OSVersion if want != got { t.Errorf("%q != %q", want, got) } for _, s := range []string{ cf.Container, cf.Config.Hostname, cf.DockerVersion, } { if s != "" { t.Errorf("non-zeroed string: %v", s) } } expectedLayerTime := time.Unix(0, 0) layers := getLayers(t, img) for _, layer := range layers { assertMTime(t, layer, expectedLayerTime) } } func TestRemoveManifests(t *testing.T) { // Load up the registry. count := 3 for i := 0; i < count; i++ { ii, err := random.Index(1024, int64(count), int64(count)) if err != nil { t.Fatal(err) } // test removing the first layer, second layer or the third layer manifest, err := ii.IndexManifest() if err != nil { t.Fatal(err) } if len(manifest.Manifests) != count { t.Fatalf("mismatched manifests on setup, had %d, expected %d", len(manifest.Manifests), count) } digest := manifest.Manifests[i].Digest ii = mutate.RemoveManifests(ii, match.Digests(digest)) manifest, err = ii.IndexManifest() if err != nil { t.Fatal(err) } if len(manifest.Manifests) != (count - 1) { t.Fatalf("mismatched manifests after removal, had %d, expected %d", len(manifest.Manifests), count-1) } for j, m := range manifest.Manifests { if m.Digest == digest { t.Fatalf("unexpectedly found removed hash %v at position %d", digest, j) } } } } func TestImageImmutability(t *testing.T) { img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) t.Run("manifest", func(t *testing.T) { // Check that Manifest is immutable. changed, err := img.Manifest() if err != nil { t.Errorf("Manifest() = %v", err) } want := changed.DeepCopy() // Create a copy of original before mutating it. changed.MediaType = types.DockerManifestList if got, err := img.Manifest(); err != nil { t.Errorf("Manifest() = %v", err) } else if !cmp.Equal(got, want) { t.Errorf("manifest changed! %s", cmp.Diff(got, want)) } }) t.Run("config file", func(t *testing.T) { // Check that ConfigFile is immutable. changed, err := img.ConfigFile() if err != nil { t.Errorf("ConfigFile() = %v", err) } want := changed.DeepCopy() // Create a copy of original before mutating it. changed.Author = "Jay Pegg" if got, err := img.ConfigFile(); err != nil { t.Errorf("ConfigFile() = %v", err) } else if !cmp.Equal(got, want) { t.Errorf("ConfigFile changed! %s", cmp.Diff(got, want)) } }) } func assertMTime(t *testing.T, layer v1.Layer, expectedTime time.Time) { l, err := layer.Uncompressed() if err != nil { t.Fatalf("reading layer failed: %v", err) } tr := tar.NewReader(l) for { header, err := tr.Next() if errors.Is(err, io.EOF) { break } if err != nil { t.Fatalf("Error reading layer: %v", err) } mtime := header.ModTime if mtime.Equal(expectedTime) == false { t.Errorf("unexpected mod time for layer. expected %v, got %v.", expectedTime, mtime) } } } func sourceImage(t *testing.T) v1.Image { return sourceImagePath(t, "testdata/source_image.tar") } func sourceImagePath(t *testing.T, tarPath string) v1.Image { t.Helper() image, err := tarball.ImageFromPath(tarPath, nil) if err != nil { t.Fatalf("Error loading image: %v", err) } return image } func getManifest(t *testing.T, i v1.Image) *v1.Manifest { t.Helper() m, err := i.Manifest() if err != nil { t.Fatalf("Error fetching image manifest: %v", err) } return m } func getLayers(t *testing.T, i v1.Image) []v1.Layer { t.Helper() l, err := i.Layers() if err != nil { t.Fatalf("Error fetching image layers: %v", err) } return l } func getConfigFile(t *testing.T, i v1.Image) *v1.ConfigFile { t.Helper() c, err := i.ConfigFile() if err != nil { t.Fatalf("Error fetching image config file: %v", err) } return c } func configFilesAreEqual(t *testing.T, first, second v1.Image) bool { t.Helper() fc := getConfigFile(t, first) sc := getConfigFile(t, second) return cmp.Equal(fc, sc) } func configDigestsAreEqual(t *testing.T, first, second v1.Image) bool { t.Helper() fm := getManifest(t, first) sm := getManifest(t, second) return fm.Config.Digest == sm.Config.Digest } func configSizesAreEqual(t *testing.T, first, second v1.Image) bool { t.Helper() fm := getManifest(t, first) sm := getManifest(t, second) return fm.Config.Size == sm.Config.Size } func manifestsAreEqual(t *testing.T, first, second v1.Image) bool { t.Helper() fm := getManifest(t, first) sm := getManifest(t, second) return cmp.Equal(fm, sm) } type mockLayer struct{} func (m mockLayer) Digest() (v1.Hash, error) { return v1.Hash{Algorithm: "fake", Hex: "digest"}, nil } func (m mockLayer) DiffID() (v1.Hash, error) { return v1.Hash{Algorithm: "fake", Hex: "diff id"}, nil } func (m mockLayer) MediaType() (types.MediaType, error) { return "some-media-type", nil } func (m mockLayer) Size() (int64, error) { return 137438691328, nil } func (m mockLayer) Compressed() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("compressed times")), nil } func (m mockLayer) Uncompressed() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("uncompressed")), nil }