summaryrefslogtreecommitdiffstats
path: root/pkg/v1/layout/write_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/v1/layout/write_test.go')
-rw-r--r--pkg/v1/layout/write_test.go672
1 files changed, 672 insertions, 0 deletions
diff --git a/pkg/v1/layout/write_test.go b/pkg/v1/layout/write_test.go
new file mode 100644
index 0000000..530e0e8
--- /dev/null
+++ b/pkg/v1/layout/write_test.go
@@ -0,0 +1,672 @@
+// Copyright 2022 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 layout
+
+import (
+ "archive/tar"
+ "bytes"
+ "io"
+ "log"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ 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/random"
+ "github.com/google/go-containerregistry/pkg/v1/stream"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/google/go-containerregistry/pkg/v1/validate"
+)
+
+func TestWrite(t *testing.T) {
+ tmp := t.TempDir()
+
+ original, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if layoutPath, err := Write(tmp, original); err != nil {
+ t.Fatalf("Write(%s) = %v", tmp, err)
+ } else if tmp != layoutPath.path() {
+ t.Fatalf("unexpected file system path %v", layoutPath)
+ }
+
+ written, err := ImageIndexFromPath(tmp)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := validate.Index(written); err != nil {
+ t.Fatalf("validate.Index() = %v", err)
+ }
+}
+
+func TestWriteErrors(t *testing.T) {
+ idx, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+
+ // Found this here:
+ // https://github.com/golang/go/issues/24195
+ invalidPath := "double-null-padded-string\x00\x00"
+ if _, err := Write(invalidPath, idx); err == nil {
+ t.Fatalf("Write(%s) = nil, expected err", invalidPath)
+ }
+}
+
+func TestAppendDescriptorInitializesIndex(t *testing.T) {
+ tmp := t.TempDir()
+ temp, err := Write(tmp, empty.Index)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Append a descriptor to a non-existent layout.
+ desc := v1.Descriptor{
+ Digest: bogusDigest,
+ Size: 1337,
+ MediaType: types.MediaType("not real"),
+ }
+ if err := temp.AppendDescriptor(desc); err != nil {
+ t.Fatalf("AppendDescriptor(%s) = %v", tmp, err)
+ }
+
+ // Read that layout from disk and make sure the descriptor is there.
+ idx, err := ImageIndexFromPath(tmp)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+
+ manifest, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatalf("IndexManifest() = %v", err)
+ }
+ if diff := cmp.Diff(manifest.Manifests[0], desc); diff != "" {
+ t.Fatalf("bad descriptor: (-got +want) %s", diff)
+ }
+}
+
+func TestRoundtrip(t *testing.T) {
+ tmp := t.TempDir()
+
+ original, err := ImageIndexFromPath(testPath)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ originalManifest, err := original.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Write it back.
+ if _, err := Write(tmp, original); err != nil {
+ t.Fatal(err)
+ }
+ reconstructed, err := ImageIndexFromPath(tmp)
+ if err != nil {
+ t.Fatalf("ImageIndexFromPath() = %v", err)
+ }
+ reconstructedManifest, err := reconstructed.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if diff := cmp.Diff(originalManifest, reconstructedManifest); diff != "" {
+ t.Fatalf("bad manifest: (-got +want) %s", diff)
+ }
+}
+
+func TestOptions(t *testing.T) {
+ tmp := t.TempDir()
+ temp, err := Write(tmp, empty.Index)
+ if err != nil {
+ t.Fatal(err)
+ }
+ annotations := map[string]string{
+ "foo": "bar",
+ }
+ urls := []string{"https://example.com"}
+ platform := v1.Platform{
+ Architecture: "mill",
+ OS: "haiku",
+ }
+ img, err := random.Image(5, 5)
+ if err != nil {
+ t.Fatal(err)
+ }
+ options := []Option{
+ WithAnnotations(annotations),
+ WithURLs(urls),
+ WithPlatform(platform),
+ }
+ if err := temp.AppendImage(img, options...); err != nil {
+ t.Fatal(err)
+ }
+ idx, err := temp.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ indexManifest, err := idx.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ desc := indexManifest.Manifests[0]
+ if got, want := desc.Annotations["foo"], "bar"; got != want {
+ t.Fatalf("wrong annotation; got: %v, want: %v", got, want)
+ }
+ if got, want := desc.URLs[0], "https://example.com"; got != want {
+ t.Fatalf("wrong urls; got: %v, want: %v", got, want)
+ }
+ if got, want := desc.Platform.Architecture, "mill"; got != want {
+ t.Fatalf("wrong Architecture; got: %v, want: %v", got, want)
+ }
+ if got, want := desc.Platform.OS, "haiku"; got != want {
+ t.Fatalf("wrong OS; got: %v, want: %v", got, want)
+ }
+}
+
+func TestDeduplicatedWrites(t *testing.T) {
+ lp, err := FromPath(testPath)
+ if err != nil {
+ t.Fatalf("FromPath() = %v", err)
+ }
+
+ b, err := lp.Blob(configDigest)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ buf := bytes.NewBuffer([]byte{})
+ if _, err := io.Copy(buf, b); err != nil {
+ log.Fatal(err)
+ }
+
+ if err := lp.WriteBlob(configDigest, io.NopCloser(bytes.NewBuffer(buf.Bytes()))); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := lp.WriteBlob(configDigest, io.NopCloser(bytes.NewBuffer(buf.Bytes()))); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRemoveDescriptor(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex
+ ii = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // add two images
+ image1, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image1); err != nil {
+ t.Fatal(err)
+ }
+ image2, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image2); err != nil {
+ t.Fatal(err)
+ }
+
+ // remove one of the images by descriptor and ensure it is correct
+ digest1, err := image1.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest2, err := image2.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.RemoveDescriptors(match.Digests(digest1)); err != nil {
+ t.Fatal(err)
+ }
+ // ensure we only have one
+ ii, err = l.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != 1 {
+ t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 1)
+ }
+ if manifest.Manifests[0].Digest != digest2 {
+ t.Fatal("removed wrong digest")
+ }
+}
+
+func TestReplaceIndex(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex
+ ii = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // add two indexes
+ index1, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendIndex(index1); err != nil {
+ t.Fatal(err)
+ }
+ index2, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendIndex(index2); err != nil {
+ t.Fatal(err)
+ }
+ index3, err := random.Index(1024, 3, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // remove one of the indexes by descriptor and ensure it is correct
+ digest1, err := index1.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest3, err := index3.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.ReplaceIndex(index3, match.Digests(digest1)); err != nil {
+ t.Fatal(err)
+ }
+ // ensure we only have one
+ ii, err = l.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != 2 {
+ t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2)
+ }
+ // we should have digest3, and *not* have digest1
+ var have3 bool
+ for _, m := range manifest.Manifests {
+ if m.Digest == digest1 {
+ t.Fatal("found digest1 still not replaced", digest1)
+ }
+ if m.Digest == digest3 {
+ have3 = true
+ }
+ }
+ if !have3 {
+ t.Fatal("could not find digest3", digest3)
+ }
+}
+
+func TestReplaceImage(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex
+ ii = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // add two images
+ image1, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image1); err != nil {
+ t.Fatal(err)
+ }
+ image2, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.AppendImage(image2); err != nil {
+ t.Fatal(err)
+ }
+ image3, err := random.Image(1024, 3)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // remove one of the images by descriptor and ensure it is correct
+ digest1, err := image1.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ digest3, err := image3.Digest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := l.ReplaceImage(image3, match.Digests(digest1)); err != nil {
+ t.Fatal(err)
+ }
+ // ensure we only have one
+ ii, err = l.ImageIndex()
+ if err != nil {
+ t.Fatal(err)
+ }
+ manifest, err := ii.IndexManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(manifest.Manifests) != 2 {
+ t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2)
+ }
+ // we should have digest3, and *not* have digest1
+ var have3 bool
+ for _, m := range manifest.Manifests {
+ if m.Digest == digest1 {
+ t.Fatal("found digest1 still not replaced", digest1)
+ }
+ if m.Digest == digest3 {
+ have3 = true
+ }
+ }
+ if !have3 {
+ t.Fatal("could not find digest3", digest3)
+ }
+}
+
+func TestRemoveBlob(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random blob
+ b := []byte("abcdefghijklmnop")
+ hash, _, err := v1.SHA256(bytes.NewReader(b))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if err := l.WriteBlob(hash, io.NopCloser(bytes.NewReader(b))); err != nil {
+ t.Fatal(err)
+ }
+ // make sure it exists
+ b2, err := l.Bytes(hash)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(b, b2) {
+ t.Fatal("mismatched bytes")
+ }
+ // now the real test, delete it
+ if err := l.RemoveBlob(hash); err != nil {
+ t.Fatal(err)
+ }
+ // now it should not exist
+ if _, err = l.Bytes(hash); err == nil {
+ t.Fatal("still existed after deletion")
+ }
+}
+
+func TestStreamingWriteLayer(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random streaming image and persist
+ pr, pw := io.Pipe()
+ tw := tar.NewWriter(pw)
+ go func() {
+ pw.CloseWithError(func() error {
+ body := "test file"
+ if err := tw.WriteHeader(&tar.Header{
+ Name: "test.txt",
+ Mode: 0600,
+ Size: int64(len(body)),
+ Typeflag: tar.TypeReg,
+ }); err != nil {
+ return err
+ }
+ if _, err := tw.Write([]byte(body)); err != nil {
+ return err
+ }
+ return tw.Close()
+ }())
+ }()
+ img, err := mutate.Append(empty.Image, mutate.Addendum{
+ Layer: stream.NewLayer(pr),
+ })
+ if err != nil {
+ t.Fatalf("creating random streaming image failed: %v", err)
+ }
+ if _, err := img.Digest(); err == nil {
+ t.Fatal("digesting image before stream is consumed; (v1.Image).Digest() = nil, expected err")
+ }
+ // AppendImage uses writeLayer
+ if err := l.AppendImage(img); err != nil {
+ t.Fatalf("(Path).AppendImage() = %v", err)
+ }
+
+ // Check that image was persisted and is valid
+ imgDigest, err := img.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Image).Digest() = %v", err)
+ }
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading image after writeLayer for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image() = %v", err)
+ }
+}
+
+func TestOverwriteWithWriteLayer(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random image and persist
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ imgDigest, err := img.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Image).Digest() = %v", err)
+ }
+ if err := l.AppendImage(img); err != nil {
+ t.Fatalf("(Path).AppendImage() = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image() = %v", err)
+ }
+
+ // get the random image's layer
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n := len(layers); n != 1 {
+ t.Fatalf("expected image with 1 layer, got %d", n)
+ }
+
+ layer := layers[0]
+ layerDigest, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Layer).Digest() = %v", err)
+ }
+
+ // truncate the layer contents on disk
+ completeLayerBytes, err := l.Bytes(layerDigest)
+ if err != nil {
+ t.Fatalf("(Path).Bytes() = %v", err)
+ }
+ truncatedLayerBytes := completeLayerBytes[:512]
+
+ path := l.path("blobs", layerDigest.Algorithm, layerDigest.Hex)
+ if err := os.WriteFile(path, truncatedLayerBytes, os.ModePerm); err != nil {
+ t.Fatalf("os.WriteFile(layerPath, truncated) = %v", err)
+ }
+
+ // ensure validation fails
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err == nil {
+ t.Fatal("validating image after truncating layer; validate.Image() = nil, expected err")
+ }
+
+ // try writing expected contents with WriteBlob
+ if err := l.WriteBlob(layerDigest, io.NopCloser(bytes.NewBuffer(completeLayerBytes))); err != nil {
+ t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).WriteBlob = %v", err)
+ }
+
+ // validation should still fail
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image after WriteBlob for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err == nil {
+ t.Fatal("validating image after attempting repair of truncated layer with WriteBlob; validate.Image() = nil, expected err")
+ }
+
+ // try writing expected contents with writeLayer
+ if err := l.writeLayer(layer); err != nil {
+ t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).writeLayer = %v", err)
+ }
+
+ // validation should now succeed
+ img, err = l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image after writeLayer for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validating image after attempting repair of truncated layer with writeLayer; validate.Image() = %v", err)
+ }
+}
+
+func TestOverwriteWithReplaceImage(t *testing.T) {
+ // need to set up a basic path
+ tmp := t.TempDir()
+
+ var ii v1.ImageIndex = empty.Index
+ l, err := Write(tmp, ii)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // create a random image and persist
+ img, err := random.Image(1024, 1)
+ if err != nil {
+ t.Fatalf("random.Image() = %v", err)
+ }
+ imgDigest, err := img.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Image).Digest() = %v", err)
+ }
+ if err := l.AppendImage(img); err != nil {
+ t.Fatalf("(Path).AppendImage() = %v", err)
+ }
+ if err := validate.Image(img); err != nil {
+ t.Fatalf("validate.Image() = %v", err)
+ }
+
+ // get the random image's layer
+ layers, err := img.Layers()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n := len(layers); n != 1 {
+ t.Fatalf("expected image with 1 layer, got %d", n)
+ }
+
+ layer := layers[0]
+ layerDigest, err := layer.Digest()
+ if err != nil {
+ t.Fatalf("(v1.Layer).Digest() = %v", err)
+ }
+
+ // truncate the layer contents on disk
+ completeLayerBytes, err := l.Bytes(layerDigest)
+ if err != nil {
+ t.Fatalf("(Path).Bytes() = %v", err)
+ }
+ truncatedLayerBytes := completeLayerBytes[:512]
+
+ path := l.path("blobs", layerDigest.Algorithm, layerDigest.Hex)
+ if err := os.WriteFile(path, truncatedLayerBytes, os.ModePerm); err != nil {
+ t.Fatalf("os.WriteFile(layerPath, truncated) = %v", err)
+ }
+
+ // ensure validation fails
+ truncatedImg, err := l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(truncatedImg); err == nil {
+ t.Fatal("validating image after truncating layer; validate.Image() = nil, expected err")
+ } else if strings.Contains(err.Error(), "unexpected EOF") {
+ t.Fatalf("validating image after truncating layer; validate.Image() error is not helpful: %v", err)
+ }
+
+ // try writing expected contents with ReplaceImage
+ if err := l.ReplaceImage(img, match.Digests(imgDigest)); err != nil {
+ t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).ReplaceImage = %v", err)
+ }
+
+ // validation should now succeed
+ repairedImg, err := l.Image(imgDigest)
+ if err != nil {
+ t.Fatalf("error loading truncated image after ReplaceImage for validation; (Path).Image = %v", err)
+ }
+ if err := validate.Image(repairedImg); err != nil {
+ t.Fatalf("validating image after attempting repair of truncated layer with ReplaceImage; validate.Image() = %v", err)
+ }
+}