diff --git a/README.md b/README.md index 18d11a32..cce0924b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ https://anuvu.github.io/zot/ * Uses [OCI image layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md) for image storage * Can serve any OCI image layout as a registry * Supports [helm charts](https://helm.sh/docs/topics/registries/) +* Supports image deletion by tag * Currently suitable for on-prem deployments (e.g. colocated with Kubernetes) * Compatible with ecosystem tools such as [skopeo](#skopeo) and [cri-o](#cri-o) * [Vulnerability scanning of images](#Scanning-images-for-known-vulnerabilities) diff --git a/pkg/compliance/v1_0_0/check.go b/pkg/compliance/v1_0_0/check.go index c87cfcc6..9124d1e8 100644 --- a/pkg/compliance/v1_0_0/check.go +++ b/pkg/compliance/v1_0_0/check.go @@ -516,6 +516,14 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(d, ShouldNotBeEmpty) So(d, ShouldEqual, digest.String()) + resp, err = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). + SetBody(content).Put(baseURL + "/v2/repo7/manifests/test:1.0.1") + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 201) + d = resp.Header().Get(api.DistContentDigestKey) + So(d, ShouldNotBeEmpty) + So(d, ShouldEqual, digest.String()) + content = []byte("this is a blob5") digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) @@ -565,11 +573,11 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeEmpty) - // delete manifest by tag should fail + // delete manifest by tag should pass resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/test:1.0") So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 400) - // delete manifest by digest + So(resp.StatusCode(), ShouldEqual, 202) + // delete manifest by digest (1.0 deleted but 1.0.1 has same reference) resp, err = resty.R().Delete(baseURL + "/v2/repo7/manifests/" + digest.String()) So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 202) @@ -880,15 +888,6 @@ func CheckWorkflows(t *testing.T, config *compliance.Config) { So(resp.StatusCode(), ShouldEqual, 200) So(resp.Body(), ShouldNotBeEmpty) - // delete manifest by tag should fail - resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/test:1.0") - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 400) - - resp, err = resty.R().Delete(baseURL + "/v2/secondtest/second/manifests/test:1.0") - So(err, ShouldBeNil) - So(resp.StatusCode(), ShouldEqual, 400) - // delete manifest by digest resp, err = resty.R().Delete(baseURL + "/v2/firsttest/first/manifests/" + digest.String()) So(err, ShouldBeNil) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 5b27d8af..46072143 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -588,11 +588,14 @@ func (is *ImageStore) DeleteImageManifest(repo string, reference string) error { return errors.ErrRepoNotFound } - // as per spec "reference" can only be a digest and not a tag + isTag := false + + // as per spec "reference" can be a digest and a tag digest, err := godigest.Parse(reference) if err != nil { - is.log.Error().Err(err).Msg("invalid reference") - return errors.ErrBadManifest + is.log.Debug().Str("invalid digest: ", reference).Msg("storage: assuming tag") + + isTag = true } is.Lock() @@ -620,7 +623,19 @@ func (is *ImageStore) DeleteImageManifest(repo string, reference string) error { outIndex.Manifests = []ispec.Descriptor{} for _, m = range index.Manifests { - if reference == m.Digest.String() { + if isTag { + tag, ok := m.Annotations[ispec.AnnotationRefName] + if ok && tag == reference { + is.log.Debug().Str("deleting tag", tag).Msg("") + + digest = m.Digest + + found = true + + continue + } + } else if reference == m.Digest.String() { + is.log.Debug().Str("deleting reference", reference).Msg("") found = true continue } @@ -657,9 +672,22 @@ func (is *ImageStore) DeleteImageManifest(repo string, reference string) error { } } - p := path.Join(dir, "blobs", digest.Algorithm().String(), digest.Encoded()) + // Delete blob only when blob digest not present in manifest entry. + // e.g. 1.0.1 & 1.0.2 have same blob digest so if we delete 1.0.1, blob should not be removed. + toDelete := true - _ = os.Remove(p) + for _, m = range outIndex.Manifests { + if digest.String() == m.Digest.String() { + toDelete = false + break + } + } + + if toDelete { + p := path.Join(dir, "blobs", digest.Algorithm().String(), digest.Encoded()) + + _ = os.Remove(p) + } return nil } diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 6cf7b5f2..4807645c 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -129,6 +129,8 @@ func TestAPIs(t *testing.T) { }) Convey("Good image manifest", func() { + annotationsMap := make(map[string]string) + annotationsMap[ispec.AnnotationRefName] = "1.0" m := ispec.Manifest{ Config: ispec.Descriptor{ Digest: d, @@ -141,29 +143,57 @@ func TestAPIs(t *testing.T) { Size: int64(l), }, }, - Annotations: map[string]string{ispec.AnnotationRefName: "1.0"}, + Annotations: annotationsMap, } + m.SchemaVersion = 2 - mb, _ = json.Marshal(m) + mb, _ := json.Marshal(m) d := godigest.FromBytes(mb) - _, err = il.PutImageManifest("test", d.String(), ispec.MediaTypeImageManifest, mb) + _, err = il.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, mb) So(err, ShouldBeNil) - _, err = il.GetImageTags("test") + _, err = il.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, mb) So(err, ShouldBeNil) + _, err = il.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, mb) + So(err, ShouldBeNil) + + // total tags should be 3 but they have same reference. + tags, err := il.GetImageTags("test") + So(err, ShouldBeNil) + So(len(tags), ShouldEqual, 3) + _, _, _, err = il.GetImageManifest("test", d.String()) So(err, ShouldBeNil) err = il.DeleteImageManifest("test", "1.0") - So(err, ShouldNotBeNil) - - err = il.DeleteBlob("test", blobDigest.String()) So(err, ShouldBeNil) + tags, err = il.GetImageTags("test") + So(err, ShouldBeNil) + So(len(tags), ShouldEqual, 2) + + // We deleted only one tag, make sure blob should not be removed. + hasBlob, _, err := il.CheckBlob("test", d.String()) + So(err, ShouldBeNil) + So(hasBlob, ShouldEqual, true) + + // If we pass reference all manifest with input reference should be deleted. err = il.DeleteImageManifest("test", d.String()) So(err, ShouldBeNil) + tags, err = il.GetImageTags("test") + So(err, ShouldBeNil) + So(len(tags), ShouldEqual, 0) + + // All tags/references are deleted, blob should not be present in disk. + hasBlob, _, err = il.CheckBlob("test", d.String()) + So(err, ShouldNotBeNil) + So(hasBlob, ShouldEqual, false) + + err = il.DeleteBlob("test", blobDigest.String()) + So(err, ShouldBeNil) + _, _, _, err = il.GetImageManifest("test", d.String()) So(err, ShouldNotBeNil) })