summaryrefslogtreecommitdiffstats
path: root/pkg/v1/mutate/mutate_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/v1/mutate/mutate_test.go')
-rw-r--r--pkg/v1/mutate/mutate_test.go770
1 files changed, 770 insertions, 0 deletions
diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go
new file mode 100644
index 0000000..c4fdba6
--- /dev/null
+++ b/pkg/v1/mutate/mutate_test.go
@@ -0,0 +1,770 @@
+// 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":null,"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
+}