// Copyright 2019 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 crane_test import ( "archive/tar" "bytes" "errors" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path" "strings" "testing" "github.com/google/go-containerregistry/internal/compare" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/crane" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/registry" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/remote" ) // TODO(jonjohnsonjr): Test crane.Copy failures. func TestCraneRegistry(t *testing.T) { // Set up a fake registry. s := httptest.NewServer(registry.New()) defer s.Close() u, err := url.Parse(s.URL) if err != nil { t.Fatal(err) } src := fmt.Sprintf("%s/test/crane", u.Host) dst := fmt.Sprintf("%s/test/crane/copy", u.Host) // Expected values. img, err := random.Image(1024, 5) if err != nil { t.Fatal(err) } digest, err := img.Digest() if err != nil { t.Fatal(err) } rawManifest, err := img.RawManifest() if err != nil { t.Fatal(err) } manifest, err := img.Manifest() if err != nil { t.Fatal(err) } config, err := img.RawConfigFile() if err != nil { t.Fatal(err) } layer, err := img.LayerByDigest(manifest.Layers[0].Digest) if err != nil { t.Fatal(err) } // Load up the registry. if err := crane.Push(img, src); err != nil { t.Fatal(err) } // Test that `crane.Foo` returns expected values. d, err := crane.Digest(src) if err != nil { t.Error(err) } else if d != digest.String() { t.Errorf("Digest(): %v != %v", d, digest) } m, err := crane.Manifest(src) if err != nil { t.Error(err) } else if string(m) != string(rawManifest) { t.Errorf("Manifest(): %v != %v", m, rawManifest) } c, err := crane.Config(src) if err != nil { t.Error(err) } else if string(c) != string(config) { t.Errorf("Config(): %v != %v", c, config) } // Make sure we pull what we pushed. pulled, err := crane.Pull(src) if err != nil { t.Error(err) } if err := compare.Images(img, pulled); err != nil { t.Fatal(err) } // Test that the copied image is the same as the source. if err := crane.Copy(src, dst); err != nil { t.Fatal(err) } // Make sure what we copied is equivalent. // Also, get options coverage in a dumb way. copied, err := crane.Pull(dst, crane.Insecure, crane.WithTransport(http.DefaultTransport), crane.WithAuth(authn.Anonymous), crane.WithAuthFromKeychain(authn.DefaultKeychain), crane.WithUserAgent("crane/tests")) if err != nil { t.Fatal(err) } if err := compare.Images(pulled, copied); err != nil { t.Fatal(err) } if err := crane.Tag(dst, "crane-tag"); err != nil { t.Fatal(err) } // Make sure what we tagged is equivalent. tagged, err := crane.Pull(fmt.Sprintf("%s:%s", dst, "crane-tag")) if err != nil { t.Fatal(err) } if err := compare.Images(pulled, tagged); err != nil { t.Fatal(err) } layerRef := fmt.Sprintf("%s/test/crane@%s", u.Host, manifest.Layers[0].Digest) pulledLayer, err := crane.PullLayer(layerRef) if err != nil { t.Fatal(err) } if err := compare.Layers(pulledLayer, layer); err != nil { t.Fatal(err) } // List Tags // dst variable have: latest and crane-tag tags, err := crane.ListTags(dst) if err != nil { t.Fatal(err) } if len(tags) != 2 { t.Fatalf("wanted 2 tags, got %d", len(tags)) } // create 4 tags for dst for i := 1; i < 5; i++ { if err := crane.Tag(dst, fmt.Sprintf("honk-tag-%d", i)); err != nil { t.Fatal(err) } } tags, err = crane.ListTags(dst) if err != nil { t.Fatal(err) } if len(tags) != 6 { t.Fatalf("wanted 6 tags, got %d", len(tags)) } // Delete the non existing image if err := crane.Delete(dst + ":honk-image"); err == nil { t.Fatal("wanted err, got nil") } // Delete the image if err := crane.Delete(src); err != nil { t.Fatal(err) } // check if the image was really deleted if _, err := crane.Pull(src); err == nil { t.Fatal("wanted err, got nil") } // check if the copied image still exist dstPulled, err := crane.Pull(dst) if err != nil { t.Fatal(err) } if err := compare.Images(dstPulled, copied); err != nil { t.Fatal(err) } // List Catalog repos, err := crane.Catalog(u.Host) if err != nil { t.Fatal(err) } if len(repos) != 2 { t.Fatalf("wanted 2 repos, got %d", len(repos)) } // Test pushing layer layer, err = img.LayerByDigest(manifest.Layers[1].Digest) if err != nil { t.Fatal(err) } if err := crane.Upload(layer, dst); err != nil { t.Fatal(err) } } func TestCraneCopyIndex(t *testing.T) { // Set up a fake registry. s := httptest.NewServer(registry.New()) defer s.Close() u, err := url.Parse(s.URL) if err != nil { t.Fatal(err) } src := fmt.Sprintf("%s/test/crane", u.Host) dst := fmt.Sprintf("%s/test/crane/copy", u.Host) // Load up the registry. idx, err := random.Index(1024, 3, 3) if err != nil { t.Fatal(err) } ref, err := name.ParseReference(src) if err != nil { t.Fatal(err) } if err := remote.WriteIndex(ref, idx); err != nil { t.Fatal(err) } // Test that the copied index is the same as the source. if err := crane.Copy(src, dst); err != nil { t.Fatal(err) } d, err := crane.Digest(src) if err != nil { t.Fatal(err) } cp, err := crane.Digest(dst) if err != nil { t.Fatal(err) } if d != cp { t.Errorf("Copied Digest(): %v != %v", d, cp) } } func TestWithPlatform(t *testing.T) { // Set up a fake registry with a platform-specific image. s := httptest.NewServer(registry.New()) defer s.Close() u, err := url.Parse(s.URL) if err != nil { t.Fatal(err) } imgs := []mutate.IndexAddendum{} for _, plat := range []string{ "linux/amd64", "linux/arm", } { img, err := crane.Image(map[string][]byte{ "platform.txt": []byte(plat), }) if err != nil { t.Fatal(err) } parts := strings.Split(plat, "/") imgs = append(imgs, mutate.IndexAddendum{ Add: img, Descriptor: v1.Descriptor{ Platform: &v1.Platform{ OS: parts[0], Architecture: parts[1], }, }, }) } idx := mutate.AppendManifests(empty.Index, imgs...) src := path.Join(u.Host, "src") dst := path.Join(u.Host, "dst") ref, err := name.ParseReference(src) if err != nil { t.Fatal(err) } // Populate registry so we can copy from it. if err := remote.WriteIndex(ref, idx); err != nil { t.Fatal(err) } if err := crane.Copy(src, dst, crane.WithPlatform(imgs[1].Platform)); err != nil { t.Fatal(err) } want, err := crane.Manifest(src, crane.WithPlatform(imgs[1].Platform)) if err != nil { t.Fatal(err) } got, err := crane.Manifest(dst) if err != nil { t.Fatal(err) } if string(got) != string(want) { t.Errorf("Manifest(%q) != Manifest(%q): (\n\n%s\n\n!=\n\n%s\n\n)", dst, src, string(got), string(want)) } arch := "real fake doors" // Now do a fake platform, should fail if _, err := crane.Manifest(src, crane.WithPlatform(&v1.Platform{ OS: "does-not-exist", Architecture: arch, })); err == nil { t.Error("crane.Manifest(fake platform): got nil want err") } else if !strings.Contains(err.Error(), arch) { t.Errorf("crane.Manifest(fake platform): expected %q in error, got: %v", arch, err) } } func TestCraneTarball(t *testing.T) { t.Parallel() // Write an image as a tarball. tmp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.Remove(tmp.Name()) img, err := random.Image(1024, 5) if err != nil { t.Fatal(err) } digest, err := img.Digest() if err != nil { t.Fatal(err) } src := fmt.Sprintf("test/crane@%s", digest) if err := crane.Save(img, src, tmp.Name()); err != nil { t.Errorf("Save: %v", err) } // Make sure the image we load has a matching digest. img, err = crane.Load(tmp.Name()) if err != nil { t.Fatal(err) } d, err := img.Digest() if err != nil { t.Fatal(err) } if d != digest { t.Errorf("digest mismatch: %v != %v", d, digest) } } func TestCraneSaveLegacy(t *testing.T) { t.Parallel() // Write an image as a legacy tarball. tmp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } defer os.Remove(tmp.Name()) img, err := random.Image(1024, 5) if err != nil { t.Fatal(err) } if err := crane.SaveLegacy(img, "test/crane", tmp.Name()); err != nil { t.Errorf("SaveOCI: %v", err) } } func TestCraneSaveOCI(t *testing.T) { t.Parallel() // Write an image as an OCI image layout. tmp := t.TempDir() img, err := random.Image(1024, 5) if err != nil { t.Fatal(err) } if err := crane.SaveOCI(img, tmp); err != nil { t.Errorf("SaveLegacy: %v", err) } } func TestCraneFilesystem(t *testing.T) { t.Parallel() tmp, err := os.CreateTemp("", "") if err != nil { t.Fatal(err) } img, err := random.Image(1024, 5) if err != nil { t.Fatal(err) } name := "/some/file" content := []byte("sentinel") tw := tar.NewWriter(tmp) if err := tw.WriteHeader(&tar.Header{ Size: int64(len(content)), Name: name, }); err != nil { t.Fatal(err) } if _, err := tw.Write(content); err != nil { t.Fatal(err) } tw.Flush() tw.Close() img, err = crane.Append(img, tmp.Name()) if err != nil { t.Fatal(err) } var buf bytes.Buffer if err := crane.Export(img, &buf); err != nil { t.Fatal(err) } tr := tar.NewReader(&buf) for { header, err := tr.Next() if errors.Is(err, io.EOF) { t.Fatalf("didn't find find") } else if err != nil { t.Fatal(err) } if header.Name == name { b, err := io.ReadAll(tr) if err != nil { t.Fatal(err) } if string(b) != string(content) { t.Fatalf("got back wrong content: %v != %v", string(b), string(content)) } break } } } func TestStreamingAppend(t *testing.T) { // Stdin will be an uncompressed layer. layer, err := crane.Layer(map[string][]byte{ "hello": []byte(`world`), }) if err != nil { t.Fatal(err) } rc, err := layer.Uncompressed() if err != nil { t.Fatal(err) } tmp, err := os.CreateTemp("", "crane-append") if err != nil { t.Fatal(err) } defer os.Remove(tmp.Name()) if _, err := io.Copy(tmp, rc); err != nil { t.Fatal(err) } stdin := os.Stdin defer func() { os.Stdin = stdin }() os.Stdin = tmp img, err := crane.Append(empty.Image, "-") if err != nil { t.Fatal(err) } ll, err := img.Layers() if err != nil { t.Fatal(err) } if want, got := 1, len(ll); want != got { t.Errorf("crane.Append(stdin) - len(layers): want %d != got %d", want, got) } } func TestBadInputs(t *testing.T) { t.Parallel() invalid := "/dev/null/@@@@@@" // Create a valid image reference that will fail with not found. s := httptest.NewServer(http.NotFoundHandler()) u, err := url.Parse(s.URL) if err != nil { t.Fatal(err) } valid404 := fmt.Sprintf("%s/some/image", u.Host) // e drops the first parameter so we can use the result of a function // that returns two values as an expression above. This is a bit of a go quirk. e := func(_ any, err error) error { return err } for _, tc := range []struct { desc string err error }{ {"Push(_, invalid)", crane.Push(nil, invalid)}, {"Upload(_, invalid)", crane.Upload(nil, invalid)}, {"Delete(invalid)", crane.Delete(invalid)}, {"Delete: 404", crane.Delete(valid404)}, {"Save(_, invalid)", crane.Save(nil, invalid, "")}, {"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, invalid, "")}, {"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, valid404, invalid)}, {"SaveOCI(_, invalid)", crane.SaveOCI(nil, "")}, {"Copy(invalid, invalid)", crane.Copy(invalid, invalid)}, {"Copy(404, invalid)", crane.Copy(valid404, invalid)}, {"Copy(404, 404)", crane.Copy(valid404, valid404)}, {"Tag(invalid, invalid)", crane.Tag(invalid, invalid)}, {"Tag(404, invalid)", crane.Tag(valid404, invalid)}, {"Tag(404, 404)", crane.Tag(valid404, valid404)}, // These return multiple values, which are hard to use as expressions. {"Pull(invalid)", e(crane.Pull(invalid))}, {"Digest(invalid)", e(crane.Digest(invalid))}, {"Manifest(invalid)", e(crane.Manifest(invalid))}, {"Config(invalid)", e(crane.Config(invalid))}, {"Config(404)", e(crane.Config(valid404))}, {"ListTags(invalid)", e(crane.ListTags(invalid))}, {"ListTags(404)", e(crane.ListTags(valid404))}, {"Append(_, invalid)", e(crane.Append(nil, invalid))}, {"Catalog(invalid)", e(crane.Catalog(invalid))}, {"Catalog(404)", e(crane.Catalog(u.Host))}, {"PullLayer(invalid)", e(crane.PullLayer(invalid))}, {"LoadTag(_, invalid)", e(crane.LoadTag("", invalid))}, {"LoadTag(invalid, 404)", e(crane.LoadTag(invalid, valid404))}, } { if tc.err == nil { t.Errorf("%s: expected err, got nil", tc.desc) } } }