diff --git a/fakes/image.go b/fakes/image.go index 75f572e2..7ee5be42 100644 --- a/fakes/image.go +++ b/fakes/image.go @@ -307,6 +307,10 @@ func (i *Image) Found() bool { return !i.deleted } +func (i *Image) Valid() bool { + return !i.deleted +} + func (i *Image) AnnotateRefName(refName string) error { i.refName = refName return nil diff --git a/image.go b/image.go index 8365ce86..5dd436c4 100644 --- a/image.go +++ b/image.go @@ -21,6 +21,8 @@ type Image interface { Env(key string) (string, error) // Found tells whether the image exists in the repository by `Name()`. Found() bool + // Valid returns true if the image is well formed (e.g. all manifest layers exist on the registry). + Valid() bool GetAnnotateRefName() (string, error) // GetLayer retrieves layer by diff id. Returns a reader of the uncompressed contents of the layer. GetLayer(diffID string) (io.ReadCloser, error) diff --git a/layout/layout.go b/layout/layout.go index 0543e3e4..6f25f46f 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -87,6 +87,10 @@ func (i *Image) Found() bool { return ImageExists(i.path) } +func (i *Image) Valid() bool { + return i.Found() +} + func ImageExists(path string) bool { if !pathExists(path) { return false diff --git a/layout/layout_test.go b/layout/layout_test.go index 271d5ed2..81a9a544 100644 --- a/layout/layout_test.go +++ b/layout/layout_test.go @@ -912,6 +912,53 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) }) + when("#Valid", func() { + var image *layout.Image + + it.Before(func() { + imagePath = filepath.Join(tmpDir, "found-image") + image, err = layout.NewImage(imagePath) + h.AssertNil(t, err) + }) + + it.After(func() { + os.RemoveAll(imagePath) + }) + + when("image doesn't exist on disk", func() { + it.Before(func() { + imagePath = filepath.Join(tmpDir, "non-exist-image") + image, err = layout.NewImage(imagePath) + h.AssertNil(t, err) + }) + + it("returns false", func() { + h.AssertTrue(t, func() bool { + return !image.Found() + }) + }) + }) + + when("image exists on disk", func() { + it.Before(func() { + imagePath = filepath.Join(testDataDir, "my-previous-image") + image, err = layout.NewImage(imagePath) + h.AssertNil(t, err) + }) + + it.After(func() { + // We don't want to delete testdata/my-previous-image + imagePath = "" + }) + + it("returns true", func() { + h.AssertTrue(t, func() bool { + return image.Found() + }) + }) + }) + }) + when("#Delete", func() { var image *layout.Image diff --git a/local/local.go b/local/local.go index ae91c329..826b4de6 100644 --- a/local/local.go +++ b/local/local.go @@ -72,6 +72,10 @@ func (i *Image) Found() bool { return i.inspect.ID != "" } +func (i *Image) Valid() bool { + return i.Found() +} + func (i *Image) GetAnnotateRefName() (string, error) { return "", nil } diff --git a/remote/remote.go b/remote/remote.go index cfa4a5cf..3b9879b5 100644 --- a/remote/remote.go +++ b/remote/remote.go @@ -15,6 +15,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/google/go-containerregistry/pkg/v1/tarball" "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" "github.com/pkg/errors" "github.com/buildpacks/imgutil" @@ -92,6 +93,7 @@ func (i *Image) Env(key string) (string, error) { func (i *Image) Found() bool { _, err := i.found() + return err == nil } @@ -104,6 +106,23 @@ func (i *Image) found() (*v1.Descriptor, error) { return remote.Head(ref, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport)) } +func (i *Image) Valid() bool { + return i.valid() == nil +} + +func (i *Image) valid() error { + reg := getRegistry(i.repoName, i.registrySettings) + ref, auth, err := referenceForRepoName(i.keychain, i.repoName, reg.insecure) + if err != nil { + return err + } + img, err := remote.Image(ref, remote.WithAuth(auth), remote.WithTransport(http.DefaultTransport)) + if err != nil { + return err + } + return validate.Image(img, validate.Fast) +} + func (i *Image) GetAnnotateRefName() (string, error) { // TODO issue https://github.com/buildpacks/imgutil/issues/178 return "", errors.New("not yet implemented") diff --git a/remote/remote_test.go b/remote/remote_test.go index b53415dc..642e86a4 100644 --- a/remote/remote_test.go +++ b/remote/remote_test.go @@ -1595,6 +1595,53 @@ func testImage(t *testing.T, when spec.G, it spec.S) { }) }) + when("#Valid", func() { + when("it exists", func() { + it("returns true", func() { + origImage, err := remote.NewImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + h.AssertNil(t, origImage.Save()) + + image, err := remote.NewImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + + h.AssertEq(t, image.Valid(), true) + }) + }) + + when("it is corrupt", func() { + it("returns false", func() { + origImage, err := remote.NewImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + tarPath, _, _ := h.RandomLayer(t, t.TempDir()) + defer os.Remove(tarPath) + h.AssertNil(t, origImage.AddLayer(tarPath)) + h.AssertNil(t, origImage.Save()) + + // delete the top layer from the registry + layers, err := origImage.UnderlyingImage().Layers() + h.AssertNil(t, err) + digest, err := layers[0].Digest() + h.AssertNil(t, err) + h.DeleteRegistryBlob(t, repoName, digest, dockerRegistry.EncodedAuth()) + + image, err := remote.NewImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + + h.AssertEq(t, image.Valid(), false) + }) + }) + + when("it does not exist", func() { + it("returns false", func() { + image, err := remote.NewImage(repoName, authn.DefaultKeychain) + h.AssertNil(t, err) + + h.AssertEq(t, image.Valid(), false) + }) + }) + }) + when("#Delete", func() { when("it exists", func() { var img imgutil.Image diff --git a/testhelpers/docker_registry.go b/testhelpers/docker_registry.go index 951d26b7..ee52cad6 100644 --- a/testhelpers/docker_registry.go +++ b/testhelpers/docker_registry.go @@ -174,7 +174,7 @@ func (r *DockerRegistry) Start(t *testing.T) { if r.username != "" { // Write Docker config and configure auth headers - writeDockerConfig(t, r.DockerDirectory, r.Host, r.Port, r.encodedAuth()) + writeDockerConfig(t, r.DockerDirectory, r.Host, r.Port, r.EncodedAuth()) } } @@ -294,7 +294,7 @@ func DockerHostname(t *testing.T) string { return "localhost" } -func (r *DockerRegistry) encodedAuth() string { +func (r *DockerRegistry) EncodedAuth() string { return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", r.username, r.password))) } diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 31974372..65fd18d6 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -13,6 +13,7 @@ import ( "io/ioutil" "math/rand" "net/http" + "net/url" "os" "path/filepath" "regexp" @@ -227,6 +228,28 @@ func PushImage(t *testing.T, dockerCli dockercli.CommonAPIClient, refStr string) AssertNil(t, err) } +// DeleteRegistryBlob deletes the blob with the given digest from the registry by issuing an HTTP DELETE request. +func DeleteRegistryBlob(t *testing.T, repoName string, digest v1.Hash, encodedAuth string) { + ref, err := name.ParseReference(repoName, name.WeakValidation) + AssertNil(t, err) + url := url.URL{ + Scheme: ref.Context().Registry.Scheme(), + Host: ref.Context().RegistryStr(), + Path: fmt.Sprintf("/v2/%s/blobs/%s", ref.Context().RepositoryStr(), digest), + } + req, err := http.NewRequest(http.MethodDelete, url.String(), nil) + AssertNil(t, err) + req.Header.Add("Authorization", "Basic "+encodedAuth) + client := &http.Client{} + resp, err := client.Do(req) + AssertNil(t, err) + defer resp.Body.Close() + + _, err = io.ReadAll(resp.Body) + AssertNil(t, err) + AssertEq(t, resp.StatusCode, http.StatusAccepted) +} + func ImageID(t *testing.T, repoName string) string { t.Helper() inspect, _, err := DockerCli(t).ImageInspectWithRaw(context.Background(), repoName)