diff --git a/.golangci.yaml b/.golangci.yaml index 94f2b1ba..7ba77097 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -92,6 +92,15 @@ linters: - legacy - std-error-handling rules: + - linters: + - godot + path: pkg/api/routes.go + - linters: + - godot + path: pkg/extensions/extension_image_trust.go + - linters: + - godot + path: pkg/extensions/extension_mgmt.go - linters: - lll - varnamelen diff --git a/errors/errors.go b/errors/errors.go index 6ce9ce2a..aca6d020 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -117,6 +117,8 @@ var ( ErrEmptyRepoName = errors.New("repo name can't be empty string") ErrEmptyTag = errors.New("tag can't be empty string") ErrEmptyDigest = errors.New("digest can't be empty string") + ErrEmptyManifestTagQuery = errors.New("empty tag query parameter") + ErrInvalidManifestTagQuery = errors.New("invalid tag query parameter: not a valid OCI/Docker tag") ErrInvalidRepoRefFormat = errors.New("invalid image reference format, use [repo:tag] or [repo@digest]") ErrLimitIsNegative = errors.New("pagination limit has negative value") ErrLimitIsExcessive = errors.New("pagination limit has excessive value") diff --git a/pkg/api/constants/consts.go b/pkg/api/constants/consts.go index ef45dda0..1afa9ad2 100644 --- a/pkg/api/constants/consts.go +++ b/pkg/api/constants/consts.go @@ -3,12 +3,23 @@ package constants import "time" const ( - RoutePrefix = "/v2" - Blobs = "blobs" - Uploads = "uploads" - DistAPIVersion = "Docker-Distribution-API-Version" - DistContentDigestKey = "Docker-Content-Digest" - SubjectDigestKey = "OCI-Subject" + RoutePrefix = "/v2" + Blobs = "blobs" + Uploads = "uploads" + DistAPIVersion = "Docker-Distribution-API-Version" + DistContentDigestKey = "Docker-Content-Digest" + // OCITagResponseKey is returned on digest manifest pushes that include tag query + // parameters (distribution-spec PR #600). + OCITagResponseKey = "OCI-Tag" + SubjectDigestKey = "OCI-Subject" + // MaxManifestDigestQueryTags is the maximum number of raw `tag=` query parameters accepted on + // PUT .../manifests/?tag=... (draft OCI distribution-spec: registries MUST support at + // least 10 and MAY respond with 414 beyond this limit). It uses the OCI tag max length (128; + // must match pkg/regexp.TagMaxLen) and an ~8KiB request-target budget, reserving 2048 bytes + // for path and digest: + // + // (8192 - 2048) / (len("tag=") + 128 + 1) == 46 + MaxManifestDigestQueryTags = (8192 - 2048) / (len("tag=") + 128 + 1) BlobUploadUUID = "Blob-Upload-UUID" DefaultMediaType = "application/json" BinaryMediaType = "application/octet-stream" diff --git a/pkg/api/constants/consts_test.go b/pkg/api/constants/consts_test.go new file mode 100644 index 00000000..579c1f21 --- /dev/null +++ b/pkg/api/constants/consts_test.go @@ -0,0 +1,18 @@ +package constants_test + +import ( + "testing" + + "zotregistry.dev/zot/v2/pkg/api/constants" + zreg "zotregistry.dev/zot/v2/pkg/regexp" +) + +func TestMaxManifestDigestQueryTagsDerived(t *testing.T) { + t.Parallel() + + want := (8192 - 2048) / (len("tag=") + zreg.TagMaxLen + 1) + + if constants.MaxManifestDigestQueryTags != want { + t.Fatalf("MaxManifestDigestQueryTags = %d, want %d", constants.MaxManifestDigestQueryTags, want) + } +} diff --git a/pkg/api/controller_test.go b/pkg/api/controller_test.go index 1ea3f56b..bf24e892 100644 --- a/pkg/api/controller_test.go +++ b/pkg/api/controller_test.go @@ -59,6 +59,7 @@ import ( extconf "zotregistry.dev/zot/v2/pkg/extensions/config" "zotregistry.dev/zot/v2/pkg/log" "zotregistry.dev/zot/v2/pkg/meta" + zreg "zotregistry.dev/zot/v2/pkg/regexp" "zotregistry.dev/zot/v2/pkg/storage" storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants" "zotregistry.dev/zot/v2/pkg/storage/gc" @@ -7808,6 +7809,142 @@ func TestManifestValidation(t *testing.T) { }) } +func TestManifestDigestQueryTags(t *testing.T) { + Convey("Manifest PUT with digest ?tag= query parameters", t, func() { + port := test.GetFreePort() + baseURL := test.GetBaseURL(port) + + conf := config.New() + conf.HTTP.Port = port + + dir := t.TempDir() + ctlr := makeController(conf, dir) + cm := test.NewControllerManager(ctlr) + cm.StartServer() + time.Sleep(1000 * time.Millisecond) + + defer cm.StopServer() + + repoName := "digest-query-tags" + img := CreateRandomImage() + manifestBytes := img.ManifestDescriptor.Data + manifestDigest := img.ManifestDescriptor.Digest + + err := UploadImage(img, baseURL, repoName, "initial") + So(err, ShouldBeNil) + + putManifestByDigest := func(rawQuery string) *resty.Response { + t.Helper() + + manifestPutURL, perr := url.Parse(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, manifestDigest.String())) + So(perr, ShouldBeNil) + manifestPutURL.RawQuery = rawQuery + + resp, rerr := resty.R(). + SetHeader("Content-Type", ispec.MediaTypeImageManifest). + SetBody(manifestBytes). + Put(manifestPutURL.String()) + So(rerr, ShouldBeNil) + + return resp + } + + Convey("multiple tag query parameters add tags and return OCI-Tag headers", func() { + manifestPutURL, err := url.Parse(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, manifestDigest.String())) + So(err, ShouldBeNil) + + q := manifestPutURL.Query() + q.Add("tag", "v1.0.0") + q.Add("tag", "v1.0") + q.Add("tag", "edge") + manifestPutURL.RawQuery = q.Encode() + + resp, err := resty.R(). + SetHeader("Content-Type", ispec.MediaTypeImageManifest). + SetBody(manifestBytes). + Put(manifestPutURL.String()) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + So(resp.Header().Get(constants.DistContentDigestKey), ShouldEqual, manifestDigest.String()) + + ociTags := resp.Header().Values(constants.OCITagResponseKey) + sort.Strings(ociTags) + So(ociTags, ShouldResemble, []string{"edge", "v1.0", "v1.0.0"}) + + for _, tag := range []string{"v1.0.0", "v1.0", "edge"} { + gresp, gerr := resty.R().Get(baseURL + fmt.Sprintf("/v2/%s/manifests/%s", repoName, tag)) + So(gerr, ShouldBeNil) + So(gresp.StatusCode(), ShouldEqual, http.StatusOK) + } + }) + + Convey("tag query with non-digest path reference returns 400", func() { + manifestPutURL, err := url.Parse(baseURL + fmt.Sprintf("/v2/%s/manifests/initial", repoName)) + So(err, ShouldBeNil) + manifestPutURL.RawQuery = "tag=notallowed" + + resp, err := resty.R(). + SetHeader("Content-Type", ispec.MediaTypeImageManifest). + SetBody(manifestBytes). + Put(manifestPutURL.String()) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + }) + + Convey("empty tag query parameter returns 400", func() { + resp := putManifestByDigest("tag=") + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + }) + + Convey("more than max tag query parameters returns 414", func() { + q := url.Values{} + for i := range constants.MaxManifestDigestQueryTags + 1 { + q.Add("tag", fmt.Sprintf("t%d", i)) + } + + resp := putManifestByDigest(q.Encode()) + So(resp.StatusCode(), ShouldEqual, http.StatusRequestURITooLong) + }) + + Convey("more than max raw tag parameters returns 414 even when values are duplicates", func() { + q := url.Values{} + for range constants.MaxManifestDigestQueryTags + 1 { + q.Add("tag", "same") + } + + resp := putManifestByDigest(q.Encode()) + So(resp.StatusCode(), ShouldEqual, http.StatusRequestURITooLong) + }) + + Convey("invalid tag query value returns 400", func() { + q := url.Values{} + q.Set("tag", "bad/ref") + + resp := putManifestByDigest(q.Encode()) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + }) + + Convey("tag query value longer than distribution-spec max length returns 400", func() { + longTag := strings.Repeat("a", zreg.TagMaxLen+1) + q := url.Values{} + q.Set("tag", longTag) + + resp := putManifestByDigest(q.Encode()) + So(resp.StatusCode(), ShouldEqual, http.StatusBadRequest) + }) + + Convey("duplicate tag query values are deduplicated in response headers", func() { + q := url.Values{} + q.Add("tag", "dup") + q.Add("tag", "dup") + + resp := putManifestByDigest(q.Encode()) + So(resp.StatusCode(), ShouldEqual, http.StatusCreated) + So(resp.Header().Values(constants.OCITagResponseKey), ShouldResemble, []string{"dup"}) + }) + }) +} + func TestArtifactReferences(t *testing.T) { Convey("Validate Artifact References", t, func() { // start a new server diff --git a/pkg/api/routes.go b/pkg/api/routes.go index 23fe3c77..c48aec85 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -266,7 +266,7 @@ func getUIHeadersHandler(config *config.Config, allowedMethods ...string) func(h // @Router /v2/ [get] // @Accept json // @Produce json -// @Success 200 {string} string "ok". +// @Success 200 {string} string "ok" func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodOptions { return @@ -304,7 +304,7 @@ func (rh *RouteHandler) CheckVersionSupport(response http.ResponseWriter, reques // @Param last query string true "last tag value for pagination" // @Success 200 {object} common.ImageTags // @Failure 404 {string} string "not found" -// @Failure 400 {string} string "bad request". +// @Failure 400 {string} string "bad request" func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodOptions { return @@ -436,9 +436,9 @@ func (rh *RouteHandler) ListTags(response http.ResponseWriter, request *http.Req // @Param name path string true "repository name" // @Param reference path string true "image reference or digest" // @Success 200 {string} string "ok" -// @Header 200 {object} constants.DistContentDigestKey +// @Header 200 {string} Docker-Content-Digest "Manifest digest of the content" // @Failure 404 {string} string "not found" -// @Failure 500 {string} string "internal server error". +// @Failure 500 {string} string "internal server error" func (rh *RouteHandler) CheckManifest(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodOptions { return @@ -509,7 +509,7 @@ type ExtensionList struct { // @Param name path string true "repository name" // @Param reference path string true "image reference or digest" // @Success 200 {object} api.ImageManifest -// @Header 200 {object} constants.DistContentDigestKey +// @Header 200 {string} Docker-Content-Digest "Manifest digest of the content" // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/manifests/{reference} [get]. @@ -676,15 +676,19 @@ func (rh *RouteHandler) GetReferrers(response http.ResponseWriter, request *http // UpdateManifest godoc // @Summary Update image manifest -// @Description Update an image's manifest given a reference or a digest +// @Description Update an image's manifest given a reference or a digest. On digest pushes with `tag=` query +// @Description parameters, 201 responses repeat the `OCI-Tag` header once per tag value. // @Accept json // @Produce json // @Param name path string true "repository name" // @Param reference path string true "image reference or digest" -// @Header 201 {object} constants.DistContentDigestKey -// @Success 201 {string} string "created" +// @Param tag query []string false "additional tag(s) for digest pushes" collectionFormat(multi) +// @Success 201 "created" +// @Header 201 {string} Docker-Content-Digest "Manifest digest of the uploaded content" +// @Header 201 {string} OCI-Tag "Echoed tag= value; this header is repeatable (one field per tag= query parameter)" // @Failure 400 {string} string "bad request" // @Failure 404 {string} string "not found" +// @Failure 414 {string} string "too many tag query parameters" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/manifests/{reference} [put]. func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *http.Request) { @@ -717,6 +721,30 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht return } + var digestQueryTags []string + + rawTagQuery := request.URL.Query()["tag"] + if len(rawTagQuery) > 0 { + if len(rawTagQuery) > constants.MaxManifestDigestQueryTags { + e := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{ + "reason": fmt.Sprintf("too many tag query parameters (max %d)", constants.MaxManifestDigestQueryTags), + }) + zcommon.WriteJSON(response, http.StatusRequestURITooLong, apiErr.NewErrorList(e)) + + return + } + + var normErr error + + digestQueryTags, normErr = normalizeManifestExtraTags(rawTagQuery) + if normErr != nil { + err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{"reason": normErr.Error()}) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(err)) + + return + } + } + body, err := io.ReadAll(request.Body) // hard to reach test case, injected error (simulates an interrupted image manifest upload) // err could be io.ErrUnexpectedEOF @@ -727,7 +755,16 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht return } - digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body) + if len(digestQueryTags) > 0 && !zcommon.IsDigest(reference) { + err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{ + "reason": "tag query parameters are only valid when pushing a manifest by digest", + }) + zcommon.WriteJSON(response, http.StatusBadRequest, apiErr.NewErrorList(err)) + + return + } + + digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body, digestQueryTags) if err != nil { details := zerr.GetDetails(err) if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain @@ -769,12 +806,22 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht } if rh.c.MetaDB != nil { - err := meta.OnUpdateManifest(request.Context(), name, reference, mediaType, - digest, body, rh.c.StoreController, rh.c.MetaDB, rh.c.Log) - if err != nil { - response.WriteHeader(http.StatusInternalServerError) + if len(digestQueryTags) > 0 { + err := meta.OnUpdateManifestDigestTags(request.Context(), name, digestQueryTags, mediaType, + digest, body, rh.c.StoreController, rh.c.MetaDB, rh.c.Log) + if err != nil { + response.WriteHeader(http.StatusInternalServerError) - return + return + } + } else { + err := meta.OnUpdateManifest(request.Context(), name, reference, mediaType, + digest, body, rh.c.StoreController, rh.c.MetaDB, rh.c.Log) + if err != nil { + response.WriteHeader(http.StatusInternalServerError) + + return + } } } @@ -784,9 +831,42 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht response.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, digest)) response.Header().Set(constants.DistContentDigestKey, digest.String()) + + for _, tag := range digestQueryTags { + response.Header().Add(constants.OCITagResponseKey, tag) //nolint:canonicalheader + } + response.WriteHeader(http.StatusCreated) } +// normalizeManifestExtraTags deduplicates tag query values in order, rejects empty components, and +// requires each value to match the OCI distribution-spec tag grammar (zreg.IsDistributionSpecTag). +func normalizeManifestExtraTags(raw []string) ([]string, error) { + seen := map[string]struct{}{} + + out := make([]string, 0, len(raw)) + + for _, rawTag := range raw { + cleanedTag := strings.TrimSpace(rawTag) + if cleanedTag == "" { + return nil, zerr.ErrEmptyManifestTagQuery + } + + if !zreg.IsDistributionSpecTag(cleanedTag) { + return nil, zerr.ErrInvalidManifestTagQuery + } + + if _, ok := seen[cleanedTag]; ok { + continue + } + + seen[cleanedTag] = struct{}{} + out = append(out, cleanedTag) + } + + return out, nil +} + // DeleteManifest godoc // @Summary Delete image manifest // @Description Delete an image's manifest given a reference or a digest @@ -794,7 +874,12 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht // @Produce json // @Param name path string true "repository name" // @Param reference path string true "image reference or digest" -// @Success 200 {string} string "ok" +// @Success 202 "accepted" +// @Failure 400 {string} string "bad request" +// @Failure 404 {string} string "not found" +// @Failure 405 {string} string "method not allowed" +// @Failure 409 {string} string "conflict" +// @Failure 500 {string} string "internal server error" // @Router /v2/{name}/manifests/{reference} [delete]. func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) @@ -933,7 +1018,7 @@ func canMount(userAc *reqCtx.UserAccessControl, imgStore storageTypes.ImageStore // @Param name path string true "repository name" // @Param digest path string true "blob/layer digest" // @Success 200 {object} api.ImageManifest -// @Header 200 {object} constants.DistContentDigestKey +// @Header 200 {string} Docker-Content-Digest "Manifest digest of the content" // @Router /v2/{name}/blobs/{digest} [head]. func (rh *RouteHandler) CheckBlob(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) @@ -1076,7 +1161,7 @@ func parseRangeHeader(contentRange string) (int64, int64, error) { // @Produce application/vnd.oci.image.layer.v1.tar+gzip // @Param name path string true "repository name" // @Param digest path string true "blob/layer digest" -// @Header 200 {object} constants.DistContentDigestKey +// @Header 200 {string} Docker-Content-Digest "Manifest digest of the content" // @Success 200 {object} api.ImageManifest // @Router /v2/{name}/blobs/{digest} [get]. func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Request) { @@ -1187,7 +1272,7 @@ func (rh *RouteHandler) GetBlob(response http.ResponseWriter, request *http.Requ // @Produce json // @Param name path string true "repository name" // @Param digest path string true "blob/layer digest" -// @Success 202 {string} string "accepted" +// @Success 202 "accepted" // @Router /v2/{name}/blobs/{digest} [delete]. func (rh *RouteHandler) DeleteBlob(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) @@ -1246,7 +1331,7 @@ func (rh *RouteHandler) DeleteBlob(response http.ResponseWriter, request *http.R // @Accept json // @Produce json // @Param name path string true "repository name" -// @Success 202 {string} string "accepted" +// @Success 202 "accepted" // @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}" // @Header 202 {string} Range "0-0" // @Failure 401 {string} string "unauthorized" @@ -1424,9 +1509,10 @@ func (rh *RouteHandler) CreateBlobUpload(response http.ResponseWriter, request * // @Produce json // @Param name path string true "repository name" // @Param session_id path string true "upload session_id" -// @Success 204 {string} string "no content" -// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}" -// @Header 202 {string} Range "0-128" +// @Success 204 "no content" +// @Header 204 {string} Location "/v2/{name}/blobs/uploads/{session_id}" +// @Header 204 {string} Range "0-128" +// @Failure 400 {string} string "bad request" // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{session_id} [get]. @@ -1485,10 +1571,10 @@ func (rh *RouteHandler) GetBlobUpload(response http.ResponseWriter, request *htt // @Produce json // @Param name path string true "repository name" // @Param session_id path string true "upload session_id" -// @Success 202 {string} string "accepted" +// @Success 202 "accepted" // @Header 202 {string} Location "/v2/{name}/blobs/uploads/{session_id}" // @Header 202 {string} Range "0-128" -// @Header 200 {object} api.BlobUploadUUID +// @Header 202 {string} Blob-Upload-UUID "Opaque blob upload session identifier" // @Failure 400 {string} string "bad request" // @Failure 404 {string} string "not found" // @Failure 416 {string} string "range not satisfiable" @@ -1585,9 +1671,9 @@ func (rh *RouteHandler) PatchBlobUpload(response http.ResponseWriter, request *h // @Param name path string true "repository name" // @Param session_id path string true "upload session_id" // @Param digest query string true "blob/layer digest" -// @Success 201 {string} string "created" -// @Header 202 {string} Location "/v2/{name}/blobs/uploads/{digest}" -// @Header 200 {object} constants.DistContentDigestKey +// @Success 201 "created" +// @Header 201 {string} Location "/v2/{name}/blobs/{digest}" +// @Header 201 {string} Docker-Content-Digest "Digest of the committed blob" // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{session_id} [put]. @@ -1742,7 +1828,7 @@ finish: // @Produce json // @Param name path string true "repository name" // @Param session_id path string true "upload session_id" -// @Success 200 {string} string "ok" +// @Success 204 "no content" // @Failure 404 {string} string "not found" // @Failure 500 {string} string "internal server error" // @Router /v2/{name}/blobs/uploads/{session_id} [delete]. @@ -1944,8 +2030,8 @@ func (rh *RouteHandler) ListExtensions(w http.ResponseWriter, r *http.Request) { // @Router /zot/auth/logout [post] // @Accept json // @Produce json -// @Success 200 {string} string "ok". -// @Failure 500 {string} string "internal server error". +// @Success 200 {string} string "ok" +// @Failure 500 {string} string "internal server error" func (rh *RouteHandler) Logout(response http.ResponseWriter, request *http.Request) { if request.Method == http.MethodOptions { return diff --git a/pkg/api/routes_test.go b/pkg/api/routes_test.go index 572c76a2..119ca488 100644 --- a/pkg/api/routes_test.go +++ b/pkg/api/routes_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" godigest "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/project-zot/mockoidc" . "github.com/smartystreets/goconvey/convey" @@ -265,7 +266,7 @@ func TestRoutes(t *testing.T) { "reference": "reference", }, &mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", zerr.ErrRepoNotFound @@ -280,7 +281,7 @@ func TestRoutes(t *testing.T) { }, &mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", zerr.ErrManifestNotFound @@ -294,7 +295,7 @@ func TestRoutes(t *testing.T) { "reference": "reference", }, &mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", zerr.ErrBadManifest @@ -308,7 +309,7 @@ func TestRoutes(t *testing.T) { "reference": "reference", }, &mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", zerr.ErrBlobNotFound @@ -323,7 +324,7 @@ func TestRoutes(t *testing.T) { "reference": "reference", }, &mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", zerr.ErrRepoBadVersion @@ -332,6 +333,101 @@ func TestRoutes(t *testing.T) { So(statusCode, ShouldEqual, http.StatusInternalServerError) }) + Convey("UpdateManifest digest query tags with MetaDB", func() { + defer func() { + ctlr.MetaDB = nil + }() + + configBlob := []byte(`{"architecture":"amd64","os":"linux"}`) + configDigest := godigest.FromBytes(configBlob) + + manifest := ispec.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + Config: ispec.Descriptor{ + MediaType: ispec.MediaTypeImageConfig, + Digest: configDigest, + Size: int64(len(configBlob)), + }, + Layers: []ispec.Descriptor{}, + } + + mcontent, mErr := json.Marshal(manifest) + So(mErr, ShouldBeNil) + + manifestDigest := godigest.FromBytes(mcontent) + digestRef := manifestDigest.String() + + ism := &mocks.MockedImageStore{ + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, extraTags []string) ( + godigest.Digest, godigest.Digest, error, + ) { + So(extraTags, ShouldResemble, []string{"meta-a", "meta-b"}) + So(string(body), ShouldEqual, string(mcontent)) + + return manifestDigest, godigest.Digest(""), nil + }, + GetBlobContentFn: func(repo string, digest godigest.Digest) ([]byte, error) { + if digest == configDigest { + return configBlob, nil + } + + return nil, zerr.ErrBlobNotFound + }, + } + ctlr.StoreController.DefaultStore = ism + + runDigestMultiTag := func(metaDB mTypes.MetaDB) *httptest.ResponseRecorder { + ctlr.MetaDB = metaDB + + reqURL := baseURL + "?tag=meta-a&tag=meta-b" + request, reqErr := http.NewRequestWithContext(context.Background(), http.MethodPut, reqURL, + bytes.NewBuffer(mcontent)) + So(reqErr, ShouldBeNil) + + request = mux.SetURLVars(request, map[string]string{ + "name": "test", + "reference": digestRef, + }) + request.Header.Add("Content-Type", ispec.MediaTypeImageManifest) + + response := httptest.NewRecorder() + rthdlr.UpdateManifest(response, request) + + return response + } + + Convey("SetRepoReference succeeds", func() { + rec := runDigestMultiTag(mocks.MetaDBMock{ + SetRepoReferenceFn: func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error { + return nil + }, + }) + + So(rec.Code, ShouldEqual, http.StatusCreated) + So(rec.Header().Values(constants.OCITagResponseKey), ShouldResemble, []string{"meta-a", "meta-b"}) + So(rec.Header().Get(constants.DistContentDigestKey), ShouldEqual, manifestDigest.String()) + }) + + Convey("SetRepoReference fails for a later tag returns 500", func() { + var calls int + + rec := runDigestMultiTag(mocks.MetaDBMock{ + SetRepoReferenceFn: func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error { + calls++ + + if reference == "meta-b" { + return ErrUnexpectedError + } + + return nil + }, + }) + + So(calls, ShouldEqual, 2) + So(rec.Code, ShouldEqual, http.StatusInternalServerError) + }) + }) + Convey("DeleteManifest", func() { testDeleteManifest := func(headers map[string]string, urlVars map[string]string, ism *mocks.MockedImageStore) int { ctlr.StoreController.DefaultStore = ism diff --git a/pkg/extensions/extension_image_trust.go b/pkg/extensions/extension_image_trust.go index d48545ea..8bf33945 100644 --- a/pkg/extensions/extension_image_trust.go +++ b/pkg/extensions/extension_image_trust.go @@ -81,8 +81,8 @@ type ImageTrust struct { // @Produce json // @Param requestBody body string true "Public key content" // @Success 200 {string} string "ok" -// @Failure 400 {string} string "bad request". -// @Failure 500 {string} string "internal server error". +// @Failure 400 {string} string "bad request" +// @Failure 500 {string} string "internal server error" func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWriter, request *http.Request) { body, err := io.ReadAll(request.Body) if err != nil { @@ -116,8 +116,8 @@ func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWrite // @Param truststoreType query string false "truststore type" // @Param requestBody body string true "Certificate content" // @Success 200 {string} string "ok" -// @Failure 400 {string} string "bad request". -// @Failure 500 {string} string "internal server error". +// @Failure 400 {string} string "bad request" +// @Failure 500 {string} string "internal server error" func (trust *ImageTrust) HandleNotationCertificateUpload(response http.ResponseWriter, request *http.Request) { var truststoreType string diff --git a/pkg/extensions/extension_mgmt.go b/pkg/extensions/extension_mgmt.go index 3cad555c..b5b49420 100644 --- a/pkg/extensions/extension_mgmt.go +++ b/pkg/extensions/extension_mgmt.go @@ -120,7 +120,7 @@ type Mgmt struct { // @Produce json // @Param resource query string false "specify resource" Enums(config) // @Success 200 {object} extensions.StrippedConfig -// @Failure 500 {string} string "internal server error". +// @Failure 500 {string} string "internal server error" func (mgmt *Mgmt) HandleGetConfig(w http.ResponseWriter, r *http.Request) { sanitizedConfig := mgmt.Conf.Sanitize() diff --git a/pkg/extensions/search/search_test.go b/pkg/extensions/search/search_test.go index 2d4f62da..9b4992e5 100644 --- a/pkg/extensions/search/search_test.go +++ b/pkg/extensions/search/search_test.go @@ -4271,7 +4271,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo So(err, ShouldBeNil) indexMultiArchMiddle1Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, - "multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob) + "multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob, nil) So(err, ShouldBeNil) image211 := CreateRandomImage() @@ -4296,7 +4296,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo So(err, ShouldBeNil) indexMultiArchMiddle2Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, - "multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob) + "multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob, nil) So(err, ShouldBeNil) image31 := CreateRandomImage() @@ -4331,7 +4331,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo So(err, ShouldBeNil) _, _, err = storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, "multiArchTop", ispec.MediaTypeImageIndex, - indexMultiArchTopBlob) + indexMultiArchTopBlob, nil) So(err, ShouldBeNil) ctlrManager.StartAndWait(port) @@ -4443,7 +4443,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo So(err, ShouldBeNil) indexMultiArchMiddle1Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, - "multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob) + "multiArchMiddle1", ispec.MediaTypeImageIndex, indexMultiArchMiddle1Blob, nil) So(err, ShouldBeNil) image211 := CreateRandomImage() @@ -4468,7 +4468,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo So(err, ShouldBeNil) indexMultiArchMiddle2Digest, _, err := storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, - "multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob) + "multiArchMiddle2", ispec.MediaTypeImageIndex, indexMultiArchMiddle2Blob, nil) So(err, ShouldBeNil) image31 := CreateRandomImage() @@ -4503,7 +4503,7 @@ func TestGlobalSearch(t *testing.T) { //nolint: gocyclo So(err, ShouldBeNil) _, _, err = storeCtlr.GetDefaultImageStore().PutImageManifest(repoName, "multiArchTop", ispec.MediaTypeImageIndex, - indexMultiArchTopBlob) + indexMultiArchTopBlob, nil) So(err, ShouldBeNil) ctlr := api.NewController(conf) @@ -5229,7 +5229,7 @@ func TestMetaDBWhenSigningImages(t *testing.T) { Convey("imageIsSignature fails", func() { // make image store ignore the wrong format of the input ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", nil @@ -6626,7 +6626,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) { Convey("imageIsSignature fails", func() { ctlr.StoreController.DefaultStore = mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", nil @@ -6652,7 +6652,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) { return configBlob, nil }, - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", nil @@ -6682,7 +6682,7 @@ func TestMetaDBWhenDeletingImages(t *testing.T) { return configBlob, nil }, - PutImageManifestFn: func(repo, reference, mediaType string, body []byte) (godigest.Digest, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest, godigest.Digest, error, ) { return "", "", ErrTestError diff --git a/pkg/extensions/sync/destination.go b/pkg/extensions/sync/destination.go index fd238899..c5367e21 100644 --- a/pkg/extensions/sync/destination.go +++ b/pkg/extensions/sync/destination.go @@ -218,7 +218,7 @@ func (registry *DestinationRegistry) copyManifest(repo string, desc ispec.Descri } digest, _, err := imageStore.PutImageManifest(repo, reference, - desc.MediaType, manifestContent) + desc.MediaType, manifestContent, nil) if err != nil { registry.log.Error().Str("errorType", common.TypeOf(err)). Err(err).Msg("couldn't upload manifest") @@ -299,7 +299,7 @@ func (registry *DestinationRegistry) copyManifest(repo string, desc ispec.Descri return firstMissingErr } - _, _, err := imageStore.PutImageManifest(repo, reference, desc.MediaType, manifestContent) + _, _, err := imageStore.PutImageManifest(repo, reference, desc.MediaType, manifestContent, nil) if err != nil { registry.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).Str("reference", reference). Err(err).Msg("failed to upload manifest") diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 25a1b10c..57e7875a 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -864,7 +864,7 @@ func TestDestinationRegistry(t *testing.T) { So(err, ShouldBeNil) digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -880,7 +880,7 @@ func TestDestinationRegistry(t *testing.T) { indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) Convey("sync index image", func() { @@ -1067,7 +1067,7 @@ func TestDestinationRegistry(t *testing.T) { So(manifestDigest, ShouldNotBeNil) // Store the manifest in the temp image store - _, _, err = tempImgStore.PutImageManifest(repoName, manifestDigest.String(), ispec.MediaTypeImageManifest, manifestContent) + _, _, err = tempImgStore.PutImageManifest(repoName, manifestDigest.String(), ispec.MediaTypeImageManifest, manifestContent, nil) So(err, ShouldBeNil) // Add to index @@ -1085,7 +1085,7 @@ func TestDestinationRegistry(t *testing.T) { So(indexDigest, ShouldNotBeNil) // Store the index manifest in the temp image store - _, _, err = tempImgStore.PutImageManifest(repoName, indexDigest.String(), ispec.MediaTypeImageIndex, indexContent) + _, _, err = tempImgStore.PutImageManifest(repoName, indexDigest.String(), ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) // Now remove one of the child manifest blobs to trigger the error @@ -1178,7 +1178,7 @@ func TestDestinationRegistry(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) Convey("sync image", func() { diff --git a/pkg/meta/hooks.go b/pkg/meta/hooks.go index 186dcd3d..c4fc6a95 100644 --- a/pkg/meta/hooks.go +++ b/pkg/meta/hooks.go @@ -2,10 +2,12 @@ package meta import ( "context" + "errors" godigest "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" + zerr "zotregistry.dev/zot/v2/errors" zcommon "zotregistry.dev/zot/v2/pkg/common" "zotregistry.dev/zot/v2/pkg/compat" "zotregistry.dev/zot/v2/pkg/log" @@ -13,6 +15,132 @@ import ( "zotregistry.dev/zot/v2/pkg/storage" ) +// priorTagManifest records where MetaDB believed each tag pointed before a digest PUT with tag= +// parameters could move it. Rollback loads manifest bytes from the blob store (GetBlobContent). +type priorTagManifest struct { + digest godigest.Digest + mediaType string +} + +// priorTagManifestsFromMetaDB returns digest and media type from RepoMeta for tags that already +// exist in metadb. Omitted tags are new or unknown to metadb. ErrRepoMetaNotFound yields an empty +// map. Rollback reads manifest blobs from storage via GetBlobContent(prior.digest). +// If a tag exists only in the image store and not in metadb, rollback cannot restore a moved tag +// (metadb and storage should stay in sync during normal operation). +func priorTagManifestsFromMetaDB(ctx context.Context, metaDB mTypes.MetaDB, repo string, tags []string, +) (map[string]priorTagManifest, error) { + empty := map[string]priorTagManifest{} + + if len(tags) == 0 { + return empty, nil + } + + repoMeta, err := metaDB.GetRepoMeta(ctx, repo) + if err != nil { + if errors.Is(err, zerr.ErrRepoMetaNotFound) { + return empty, nil + } + + return nil, err + } + + if len(repoMeta.Tags) == 0 { + return empty, nil + } + + out := make(map[string]priorTagManifest, len(tags)) + + for _, tag := range tags { + desc, ok := repoMeta.Tags[tag] + if !ok || desc.Digest == "" { + continue + } + + dgst, parseErr := godigest.Parse(desc.Digest) + if parseErr != nil { + continue + } + + descMediaType := desc.MediaType + if descMediaType == "" { + descMediaType = v1.MediaTypeImageManifest + } + + out[tag] = priorTagManifest{ + digest: dgst, + mediaType: descMediaType, + } + } + + return out, nil +} + +// rollbackDigestManifestTags deletes every tag in tags from the image store (this PUT added them to the +// index). It runs OnDeleteManifest only for tags in appliedMetaTags: those had a successful meta update for +// digest and must be reverted. Calling OnDeleteManifest for other tags is unsafe—RemoveRepoReference can +// drop a tag entry even when metadb still maps that tag to a different digest (e.g. meta not updated yet). +// When priorTagManifests has an entry for a tag, it re-applies that manifest so moved tags point at their +// original digest again. +func rollbackDigestManifestTags(ctx context.Context, repo string, tags, appliedMetaTags []string, mediaType string, + digest godigest.Digest, + body []byte, storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger, + priorTagManifests map[string]priorTagManifest, +) { + imgStore := storeController.GetImageStore(repo) + + for i := len(tags) - 1; i >= 0; i-- { + refTag := tags[i] + if delErr := imgStore.DeleteImageManifest(repo, refTag, false); delErr != nil && + !errors.Is(delErr, zerr.ErrManifestNotFound) { + log.Error().Err(delErr).Str("repository", repo).Str("tag", refTag). + Msg("multi-tag digest push: rollback DeleteImageManifest failed") + } + } + + for i := len(appliedMetaTags) - 1; i >= 0; i-- { + refTag := appliedMetaTags[i] + + metaDelErr := OnDeleteManifest(repo, refTag, mediaType, digest, body, storeController, metaDB, log) + if metaDelErr != nil { + log.Error().Err(metaDelErr).Str("repository", repo).Str("tag", refTag). + Msg("multi-tag digest push: rollback OnDeleteManifest failed") + } + } + + if len(priorTagManifests) == 0 { + return + } + + for _, refTag := range tags { + prior, ok := priorTagManifests[refTag] + if !ok { + continue + } + + restoreBody, blobErr := imgStore.GetBlobContent(repo, prior.digest) + if blobErr != nil { + log.Error().Err(blobErr).Str("repository", repo).Str("tag", refTag). + Msg("multi-tag digest push: rollback load prior manifest blob failed") + + continue + } + + if _, _, putErr := imgStore.PutImageManifest(repo, prior.digest.String(), prior.mediaType, restoreBody, + []string{refTag}); putErr != nil { + log.Error().Err(putErr).Str("repository", repo).Str("tag", refTag). + Msg("multi-tag digest push: rollback restore prior manifest in store failed") + + continue + } + + if metaErr := OnUpdateManifest(ctx, repo, refTag, prior.mediaType, prior.digest, restoreBody, + storeController, metaDB, log); metaErr != nil { + log.Error().Err(metaErr).Str("repository", repo).Str("tag", refTag). + Msg("multi-tag digest push: rollback restore prior metadb failed") + } + } +} + // OnUpdateManifest is called when a new manifest is added. It updates metadb according to the type // of image pushed(normal images, signatues, etc.). In care of any errors, it makes sure to keep // consistency between metadb and the image store. @@ -43,6 +171,43 @@ func OnUpdateManifest(ctx context.Context, repo, reference, mediaType string, di return nil } +// OnUpdateManifestDigestTags updates metadb for each tag from a digest-addressed manifest push that used +// repeated `tag=` query parameters. It snapshots each tag's prior digest and media type from MetaDB +// (GetRepoMeta) before updates, then calls OnUpdateManifest per tag; on the first failure it removes +// every tag in tags from the image store, reverts MetaDB only for tags that had already completed +// OnUpdateManifest successfully, and restores moved tags using the snapshot (see rollbackDigestManifestTags). +func OnUpdateManifestDigestTags(ctx context.Context, repo string, tags []string, mediaType string, + digest godigest.Digest, body []byte, + storeController storage.StoreController, metaDB mTypes.MetaDB, log log.Logger, +) error { + if len(tags) == 0 { + return nil + } + + priorTagManifests, err := priorTagManifestsFromMetaDB(ctx, metaDB, repo, tags) + if err != nil { + return err + } + + applied := make([]string, 0, len(tags)) + + for _, tag := range tags { + if err := OnUpdateManifest(ctx, repo, tag, mediaType, digest, body, storeController, metaDB, log); err != nil { + log.Error().Err(err).Str("repository", repo).Str("tag", tag). + Msg("multi-tag digest push: meta update failed; rolling back tag query additions") + + rollbackDigestManifestTags(ctx, repo, tags, applied, mediaType, digest, body, storeController, metaDB, log, + priorTagManifests) + + return err + } + + applied = append(applied, tag) + } + + return nil +} + // OnDeleteManifest is called when a manifest is deleted. It updates metadb according to the type // of image pushed(normal images, signatues, etc.). In care of any errors, it makes sure to keep // consistency between metadb and the image store. @@ -82,7 +247,7 @@ func OnDeleteManifest(repo, reference, mediaType string, digest godigest.Digest, log.Info().Str("component", "metadb").Msg("restoring image store") // restore image store - _, _, err := imgStore.PutImageManifest(repo, reference, mediaType, manifestBlob) + _, _, err := imgStore.PutImageManifest(repo, reference, mediaType, manifestBlob, nil) if err != nil { log.Error().Err(err).Str("component", "metadb"). Msg("failed to restore manifest to image store, database is not consistent") diff --git a/pkg/meta/hooks_internal_test.go b/pkg/meta/hooks_internal_test.go new file mode 100644 index 00000000..b99d532f --- /dev/null +++ b/pkg/meta/hooks_internal_test.go @@ -0,0 +1,262 @@ +package meta + +import ( + "context" + "encoding/json" + "errors" + "testing" + + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + + zerr "zotregistry.dev/zot/v2/errors" + "zotregistry.dev/zot/v2/pkg/log" + mTypes "zotregistry.dev/zot/v2/pkg/meta/types" + "zotregistry.dev/zot/v2/pkg/storage" + testimage "zotregistry.dev/zot/v2/pkg/test/image-utils" + "zotregistry.dev/zot/v2/pkg/test/mocks" +) + +var errHookInternal = errors.New("hook internal test error") + +func TestPriorTagManifestsFromMetaDB(t *testing.T) { + Convey("priorTagManifestsFromMetaDB", t, func() { + ctx := context.Background() + + Convey("empty tags", func() { + out, err := priorTagManifestsFromMetaDB(ctx, mocks.MetaDBMock{}, "repo", nil) + So(err, ShouldBeNil) + So(len(out), ShouldEqual, 0) + }) + + Convey("repo meta not found yields empty map", func() { + db := mocks.MetaDBMock{ + GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) { + return mTypes.RepoMeta{}, zerr.ErrRepoMetaNotFound + }, + } + + out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"}) + So(err, ShouldBeNil) + So(len(out), ShouldEqual, 0) + }) + + Convey("get repo meta error propagates", func() { + db := mocks.MetaDBMock{ + GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) { + return mTypes.RepoMeta{}, errHookInternal + }, + } + + _, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"}) + So(errors.Is(err, errHookInternal), ShouldBeTrue) + }) + + Convey("empty tag map in repo meta", func() { + db := mocks.MetaDBMock{ + GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) { + return mTypes.RepoMeta{Tags: map[mTypes.Tag]mTypes.Descriptor{}}, nil + }, + } + + out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"}) + So(err, ShouldBeNil) + So(len(out), ShouldEqual, 0) + }) + + Convey("skips unknown tag empty digest invalid digest", func() { + good := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + db := mocks.MetaDBMock{ + GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) { + return mTypes.RepoMeta{ + Tags: map[mTypes.Tag]mTypes.Descriptor{ + "only-good": {Digest: good, MediaType: ispec.MediaTypeImageManifest}, + "empty-dig": {Digest: "", MediaType: ispec.MediaTypeImageManifest}, + "bad-dig": {Digest: "not-a-digest", MediaType: ispec.MediaTypeImageManifest}, + }, + }, nil + }, + } + + tags := []string{"missing", "only-good", "empty-dig", "bad-dig"} + out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", tags) + So(err, ShouldBeNil) + So(len(out), ShouldEqual, 1) + + pm, ok := out["only-good"] + So(ok, ShouldBeTrue) + So(pm.digest.String(), ShouldEqual, good) + So(pm.mediaType, ShouldEqual, ispec.MediaTypeImageManifest) + }) + + Convey("default media type when descriptor empty", func() { + good := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + + db := mocks.MetaDBMock{ + GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) { + return mTypes.RepoMeta{ + Tags: map[mTypes.Tag]mTypes.Descriptor{ + "t": {Digest: good, MediaType: ""}, + }, + }, nil + }, + } + + out, err := priorTagManifestsFromMetaDB(ctx, db, "repo", []string{"t"}) + So(err, ShouldBeNil) + So(out["t"].mediaType, ShouldEqual, ispec.MediaTypeImageManifest) + }) + }) +} + +func TestRollbackDigestManifestTags(t *testing.T) { + Convey("rollbackDigestManifestTags", t, func() { + ctx := context.Background() + testLog := log.NewTestLogger() + + img := testimage.CreateDefaultImage() + mediaType := img.ManifestDescriptor.MediaType + if mediaType == "" { + mediaType = ispec.MediaTypeImageManifest + } + + body := img.ManifestDescriptor.Data + dgst := img.Digest() + + Convey("delete manifest error is logged path", func() { + var deleteCalls int + + is := mocks.MockedImageStore{ + DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error { + deleteCalls++ + + return errors.New("delete failed") + }, + } + + sc := storage.StoreController{DefaultStore: &is} + rollbackDigestManifestTags(ctx, "repo", []string{"a"}, nil, mediaType, dgst, body, sc, + mocks.MetaDBMock{}, testLog, nil) + + So(deleteCalls, ShouldEqual, 1) + }) + + Convey("delete manifest not found is ignored", func() { + is := mocks.MockedImageStore{ + DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error { + return zerr.ErrManifestNotFound + }, + } + + sc := storage.StoreController{DefaultStore: &is} + rollbackDigestManifestTags(ctx, "repo", []string{"a"}, nil, mediaType, dgst, body, sc, + mocks.MetaDBMock{}, testLog, nil) + }) + + Convey("on delete manifest failure is logged", func() { + is := mocks.MockedImageStore{} + + metaDB := mocks.MetaDBMock{ + RemoveRepoReferenceFn: func(string, string, godigest.Digest) error { + return errors.New("remove failed") + }, + } + + sc := storage.StoreController{DefaultStore: &is} + rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc, + metaDB, testLog, nil) + }) + + Convey("prior restore get blob fails", func() { + other := testimage.CreateRandomImage() + priorD := (&other).Digest() + prior := map[string]priorTagManifest{ + "t": {digest: priorD, mediaType: mediaType}, + } + + is := mocks.MockedImageStore{ + DeleteImageManifestFn: func(string, string, bool) error { return nil }, + GetBlobContentFn: func(string, godigest.Digest) ([]byte, error) { + return nil, errors.New("blob missing") + }, + } + + sc := storage.StoreController{DefaultStore: &is} + rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc, + mocks.MetaDBMock{}, testLog, prior) + }) + + Convey("prior restore put manifest fails", func() { + priorBody := body + priorD := dgst + prior := map[string]priorTagManifest{ + "t": {digest: priorD, mediaType: mediaType}, + } + + is := mocks.MockedImageStore{ + DeleteImageManifestFn: func(string, string, bool) error { return nil }, + GetBlobContentFn: func(_ string, blobDigest godigest.Digest) ([]byte, error) { + So(blobDigest, ShouldResemble, priorD) + + return priorBody, nil + }, + PutImageManifestFn: func(string, string, string, []byte, []string) (godigest.Digest, godigest.Digest, error) { + return "", "", errors.New("put failed") + }, + } + + sc := storage.StoreController{DefaultStore: &is} + rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc, + mocks.MetaDBMock{}, testLog, prior) + }) + + Convey("prior restore metadb update fails", func() { + priorBody := body + priorD := dgst + prior := map[string]priorTagManifest{ + "t": {digest: priorD, mediaType: mediaType}, + } + + var manifest ispec.Manifest + err := json.Unmarshal(priorBody, &manifest) + So(err, ShouldBeNil) + + configBytes, err := json.Marshal(img.Config) + So(err, ShouldBeNil) + + metaDB := mocks.MetaDBMock{ + SetRepoReferenceFn: func(context.Context, string, string, mTypes.ImageMeta) error { + return errors.New("set ref failed") + }, + } + + is := mocks.MockedImageStore{ + DeleteImageManifestFn: func(string, string, bool) error { return nil }, + GetBlobContentFn: func(_ string, blobDigest godigest.Digest) ([]byte, error) { + switch { + case blobDigest == priorD: + return priorBody, nil + case blobDigest == manifest.Config.Digest: + return configBytes, nil + default: + So(blobDigest.String(), ShouldBeIn, + []string{priorD.String(), manifest.Config.Digest.String()}) + } + + return nil, nil + }, + PutImageManifestFn: func(_, _, _ string, blob []byte, _ []string) (godigest.Digest, godigest.Digest, error) { + d := godigest.FromBytes(blob) + + return d, d, nil + }, + } + + sc := storage.StoreController{DefaultStore: &is} + rollbackDigestManifestTags(ctx, "repo", []string{"t"}, []string{"t"}, mediaType, dgst, body, sc, + metaDB, testLog, prior) + }) + }) +} diff --git a/pkg/meta/hooks_test.go b/pkg/meta/hooks_test.go index 2e3c1d92..8aa0a46f 100644 --- a/pkg/meta/hooks_test.go +++ b/pkg/meta/hooks_test.go @@ -5,20 +5,412 @@ import ( "errors" "testing" + godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" + zerr "zotregistry.dev/zot/v2/errors" "zotregistry.dev/zot/v2/pkg/extensions/monitoring" "zotregistry.dev/zot/v2/pkg/log" "zotregistry.dev/zot/v2/pkg/meta" "zotregistry.dev/zot/v2/pkg/meta/boltdb" + mTypes "zotregistry.dev/zot/v2/pkg/meta/types" "zotregistry.dev/zot/v2/pkg/storage" "zotregistry.dev/zot/v2/pkg/storage/local" + stypes "zotregistry.dev/zot/v2/pkg/storage/types" . "zotregistry.dev/zot/v2/pkg/test/image-utils" "zotregistry.dev/zot/v2/pkg/test/mocks" ) -var ErrTestError = errors.New("test error") +var ( + errDeleteAfterMetaHookTest = errors.New("delete manifest after meta failure hook test") + errDigestTagsSetRepoReferenceFail = errors.New("injected SetRepoReference failure for digest-tags rollback tests") + errGetRepoMetaForDigestTags = errors.New("get repo meta failed for digest tags test") + errSetRepoRefForHookTest = errors.New("set repo reference failed for hook test") +) + +// setRepoRefFailMetaDB delegates to an inner MetaDB but fails SetRepoReference for one tag (used to +// exercise multi-tag digest rollback after a partial OnUpdateManifest success). +type setRepoRefFailMetaDB struct { + mTypes.MetaDB + + failRef string +} + +func (w *setRepoRefFailMetaDB) SetRepoReference( + ctx context.Context, repo, ref string, imageMeta mTypes.ImageMeta, +) error { + if ref == w.failRef { + return errDigestTagsSetRepoReferenceFail + } + + return w.MetaDB.SetRepoReference(ctx, repo, ref, imageMeta) +} + +// failDeleteImageStore delegates to an inner ImageStore but forces DeleteImageManifest to return deleteErr. +type failDeleteImageStore struct { + stypes.ImageStore + + deleteErr error +} + +func (f *failDeleteImageStore) DeleteImageManifest(repo, reference string, detectCollision bool) error { + return f.deleteErr +} + +func TestOnUpdateManifestDigestTags_emptyTags(t *testing.T) { + Convey("OnUpdateManifestDigestTags with no tags is a no-op (nil MetaDB: no GetRepoMeta/SetRepoReference path)", + t, func() { + log := log.NewTestLogger() + + err := meta.OnUpdateManifestDigestTags(context.Background(), "repo", nil, ispec.MediaTypeImageManifest, + godigest.Digest(""), nil, storage.StoreController{}, nil, log) + So(err, ShouldBeNil) + }) +} + +func TestOnUpdateManifestDigestTags_success(t *testing.T) { + Convey("OnUpdateManifestDigestTags updates metadb for each digest query tag", t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewTestLogger() + metrics := monitoring.NewMetricsServer(false, log) + + defer metrics.Stop() + storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil) + + params := boltdb.DBParameters{RootDir: rootDir} + boltDriver, err := boltdb.GetBoltDriver(params) + So(err, ShouldBeNil) + + metaDB, err := boltdb.New(boltDriver, log) + So(err, ShouldBeNil) + + image := CreateDefaultImage() + mediaType := image.ManifestDescriptor.MediaType + if mediaType == "" { + mediaType = ispec.MediaTypeImageManifest + } + + manifestBody := image.ManifestDescriptor.Data + manifestDigest := image.Digest() + + err = WriteImageToFileSystem(image, "repo", "seed", storeController) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifest(context.Background(), "repo", "seed", mediaType, manifestDigest, manifestBody, + storeController, metaDB, log) + So(err, ShouldBeNil) + + imgStore := storeController.GetImageStore("repo") + _, _, err = imgStore.PutImageManifest("repo", manifestDigest.String(), mediaType, manifestBody, + []string{"ta", "tb"}) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"ta", "tb"}, mediaType, + manifestDigest, manifestBody, storeController, metaDB, log) + So(err, ShouldBeNil) + + wantDigest := manifestDigest.String() + + repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "ta") + So(repoMeta.Tags, ShouldContainKey, "tb") + So(repoMeta.Tags, ShouldContainKey, "seed") + So(repoMeta.Tags["ta"].Digest, ShouldEqual, wantDigest) + So(repoMeta.Tags["tb"].Digest, ShouldEqual, wantDigest) + So(repoMeta.Tags["seed"].Digest, ShouldEqual, wantDigest) + }) +} + +func TestOnUpdateManifestDigestTags_rollbackPartialMeta(t *testing.T) { + Convey("OnUpdateManifestDigestTags rollback deletes all new index tags; meta rollback only for applied tags", + t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewTestLogger() + metrics := monitoring.NewMetricsServer(false, log) + + defer metrics.Stop() + storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil) + + params := boltdb.DBParameters{RootDir: rootDir} + boltDriver, err := boltdb.GetBoltDriver(params) + So(err, ShouldBeNil) + + metaDB, err := boltdb.New(boltDriver, log) + So(err, ShouldBeNil) + + image := CreateDefaultImage() + mediaType := image.ManifestDescriptor.MediaType + if mediaType == "" { + mediaType = ispec.MediaTypeImageManifest + } + + manifestBody := image.ManifestDescriptor.Data + manifestDigest := image.Digest() + + err = WriteImageToFileSystem(image, "repo", "seed", storeController) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifest(context.Background(), "repo", "seed", mediaType, manifestDigest, manifestBody, + storeController, metaDB, log) + So(err, ShouldBeNil) + + imgStore := storeController.GetImageStore("repo") + _, _, err = imgStore.PutImageManifest("repo", manifestDigest.String(), mediaType, manifestBody, + []string{"ta", "tb"}) + So(err, ShouldBeNil) + + repoMetaBefore, err := metaDB.GetRepoMeta(context.Background(), "repo") + So(err, ShouldBeNil) + seedDigestBefore := repoMetaBefore.Tags["seed"].Digest + So(seedDigestBefore, ShouldEqual, manifestDigest.String()) + + wrapped := &setRepoRefFailMetaDB{MetaDB: metaDB, failRef: "tb"} + + err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"ta", "tb"}, mediaType, + manifestDigest, manifestBody, storeController, wrapped, log) + So(err, ShouldEqual, errDigestTagsSetRepoReferenceFail) + + _, _, _, err = imgStore.GetImageManifest("repo", "ta") + So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue) + _, _, _, err = imgStore.GetImageManifest("repo", "tb") + So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue) + + seedBody, _, _, err := imgStore.GetImageManifest("repo", "seed") + So(err, ShouldBeNil) + So(godigest.FromBytes(seedBody).String(), ShouldEqual, manifestDigest.String()) + + repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldNotContainKey, "ta") + So(repoMeta.Tags, ShouldNotContainKey, "tb") + So(repoMeta.Tags, ShouldContainKey, "seed") + So(repoMeta.Tags["seed"].Digest, ShouldEqual, seedDigestBefore) + }) +} + +func TestOnUpdateManifestDigestTags_rollbackRestoresMovedTag(t *testing.T) { + Convey("rollback restores a tag moved from digest A to digest B back to digest A when MetaDB fails later", + t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewTestLogger() + metrics := monitoring.NewMetricsServer(false, log) + + defer metrics.Stop() + storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil) + + params := boltdb.DBParameters{RootDir: rootDir} + boltDriver, err := boltdb.GetBoltDriver(params) + So(err, ShouldBeNil) + + metaDB, err := boltdb.New(boltDriver, log) + So(err, ShouldBeNil) + + imageA := CreateDefaultImage() + imageB := CreateRandomImage() + So(imageA.Digest(), ShouldNotEqual, imageB.Digest()) + + mediaTypeA := imageA.ManifestDescriptor.MediaType + if mediaTypeA == "" { + mediaTypeA = ispec.MediaTypeImageManifest + } + + mediaTypeB := imageB.ManifestDescriptor.MediaType + if mediaTypeB == "" { + mediaTypeB = ispec.MediaTypeImageManifest + } + + bodyA := imageA.ManifestDescriptor.Data + digestA := imageA.Digest() + bodyB := imageB.ManifestDescriptor.Data + digestB := imageB.Digest() + + err = WriteImageToFileSystem(imageA, "repo", "movable", storeController) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifest(context.Background(), "repo", "movable", mediaTypeA, digestA, bodyA, + storeController, metaDB, log) + So(err, ShouldBeNil) + + err = WriteImageToFileSystem(imageB, "repo", "yardB", storeController) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifest(context.Background(), "repo", "yardB", mediaTypeB, digestB, bodyB, + storeController, metaDB, log) + So(err, ShouldBeNil) + + imgStore := storeController.GetImageStore("repo") + + _, _, err = imgStore.PutImageManifest("repo", digestB.String(), mediaTypeB, bodyB, + []string{"movable", "onlyB"}) + So(err, ShouldBeNil) + + wrapped := &setRepoRefFailMetaDB{MetaDB: metaDB, failRef: "onlyB"} + + err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"movable", "onlyB"}, mediaTypeB, + digestB, bodyB, storeController, wrapped, log) + So(err, ShouldEqual, errDigestTagsSetRepoReferenceFail) + + movableBody, movableD, _, err := imgStore.GetImageManifest("repo", "movable") + So(err, ShouldBeNil) + So(movableD.String(), ShouldEqual, digestA.String()) + So(godigest.FromBytes(movableBody).String(), ShouldEqual, digestA.String()) + + _, _, _, err = imgStore.GetImageManifest("repo", "onlyB") + So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue) + + repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags["movable"].Digest, ShouldEqual, digestA.String()) + So(repoMeta.Tags["yardB"].Digest, ShouldEqual, digestB.String()) + So(repoMeta.Tags, ShouldNotContainKey, "onlyB") + }) +} + +func TestOnUpdateManifestDigestTags_getRepoMetaError(t *testing.T) { + Convey("OnUpdateManifestDigestTags returns when GetRepoMeta fails with a non-ErrRepoMetaNotFound error", t, func() { + log := log.NewTestLogger() + metaDB := mocks.MetaDBMock{ + GetRepoMetaFn: func(context.Context, string) (mTypes.RepoMeta, error) { + return mTypes.RepoMeta{}, errGetRepoMetaForDigestTags + }, + } + + d := godigest.FromString("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + + err := meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"a"}, ispec.MediaTypeImageManifest, + d, []byte("{}"), storage.StoreController{}, metaDB, log) + So(errors.Is(err, errGetRepoMetaForDigestTags), ShouldBeTrue) + }) +} + +func TestOnUpdateManifestDigestTags_whenRepoMetaMissing(t *testing.T) { + Convey("ErrRepoMetaNotFound during snapshot still allows digest query tag meta updates", t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewTestLogger() + metrics := monitoring.NewMetricsServer(false, log) + + defer metrics.Stop() + storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil) + + params := boltdb.DBParameters{RootDir: rootDir} + boltDriver, err := boltdb.GetBoltDriver(params) + So(err, ShouldBeNil) + + metaDB, err := boltdb.New(boltDriver, log) + So(err, ShouldBeNil) + + image := CreateDefaultImage() + mediaType := image.ManifestDescriptor.MediaType + if mediaType == "" { + mediaType = ispec.MediaTypeImageManifest + } + + manifestBody := image.ManifestDescriptor.Data + manifestDigest := image.Digest() + + err = WriteImageToFileSystem(image, "repo", "seed", storeController) + So(err, ShouldBeNil) + + _, err = metaDB.GetRepoMeta(context.Background(), "repo") + So(errors.Is(err, zerr.ErrRepoMetaNotFound), ShouldBeTrue) + + imgStore := storeController.GetImageStore("repo") + _, _, err = imgStore.PutImageManifest("repo", manifestDigest.String(), mediaType, manifestBody, + []string{"ta", "tb"}) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifestDigestTags(context.Background(), "repo", []string{"ta", "tb"}, mediaType, + manifestDigest, manifestBody, storeController, metaDB, log) + So(err, ShouldBeNil) + + wantDigest := manifestDigest.String() + + repoMeta, err := metaDB.GetRepoMeta(context.Background(), "repo") + So(err, ShouldBeNil) + So(repoMeta.Tags, ShouldContainKey, "ta") + So(repoMeta.Tags, ShouldContainKey, "tb") + So(repoMeta.Tags, ShouldNotContainKey, "seed") + So(repoMeta.Tags["ta"].Digest, ShouldEqual, wantDigest) + So(repoMeta.Tags["tb"].Digest, ShouldEqual, wantDigest) + }) +} + +func TestOnUpdateManifest_setRepoReferenceFailsRemovesManifest(t *testing.T) { + Convey("OnUpdateManifest deletes the manifest from the store when SetRepoReference fails", t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewTestLogger() + metrics := monitoring.NewMetricsServer(false, log) + + defer metrics.Stop() + storeController.DefaultStore = local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil) + + metaDB := mocks.MetaDBMock{ + SetRepoReferenceFn: func(context.Context, string, string, mTypes.ImageMeta) error { + return errSetRepoRefForHookTest + }, + } + + image := CreateDefaultImage() + mediaType := image.ManifestDescriptor.MediaType + if mediaType == "" { + mediaType = ispec.MediaTypeImageManifest + } + + err := WriteImageToFileSystem(image, "repo", "tag1", storeController) + So(err, ShouldBeNil) + + imgStore := storeController.GetImageStore("repo") + + err = meta.OnUpdateManifest(context.Background(), "repo", "tag1", mediaType, image.Digest(), + image.ManifestDescriptor.Data, storeController, metaDB, log) + So(errors.Is(err, errSetRepoRefForHookTest), ShouldBeTrue) + + _, _, _, err = imgStore.GetImageManifest("repo", "tag1") + So(errors.Is(err, zerr.ErrManifestNotFound), ShouldBeTrue) + }) +} + +func TestOnUpdateManifest_whenDeleteAfterMetaFailureFails(t *testing.T) { + Convey("OnUpdateManifest returns the delete error when meta fails and store cleanup fails", t, func() { + rootDir := t.TempDir() + storeController := storage.StoreController{} + log := log.NewTestLogger() + metrics := monitoring.NewMetricsServer(false, log) + + defer metrics.Stop() + baseStore := local.NewImageStore(rootDir, true, true, log, metrics, nil, nil, nil, nil) + storeController.DefaultStore = &failDeleteImageStore{ + ImageStore: baseStore, + deleteErr: errDeleteAfterMetaHookTest, + } + + metaDB := mocks.MetaDBMock{ + SetRepoReferenceFn: func(context.Context, string, string, mTypes.ImageMeta) error { + return errSetRepoRefForHookTest + }, + } + + image := CreateDefaultImage() + mediaType := image.ManifestDescriptor.MediaType + if mediaType == "" { + mediaType = ispec.MediaTypeImageManifest + } + + err := WriteImageToFileSystem(image, "repo", "tag1", storeController) + So(err, ShouldBeNil) + + err = meta.OnUpdateManifest(context.Background(), "repo", "tag1", mediaType, image.Digest(), + image.ManifestDescriptor.Data, storeController, metaDB, log) + So(errors.Is(err, errDeleteAfterMetaHookTest), ShouldBeTrue) + }) +} func TestOnUpdateManifest(t *testing.T) { Convey("On UpdateManifest", t, func() { diff --git a/pkg/meta/parse_test.go b/pkg/meta/parse_test.go index 9e3bd45b..827e38e1 100644 --- a/pkg/meta/parse_test.go +++ b/pkg/meta/parse_test.go @@ -40,13 +40,16 @@ import ( const repo = "repo" +// errMetaTestInjected is returned from mocks in this file to assert error paths in ParseStorage and related tests. +var errMetaTestInjected = errors.New("injected error for parse_test mocks") + func TestParseStorageErrors(t *testing.T) { ctx := context.Background() Convey("ParseStorage", t, func() { imageStore := mocks.MockedImageStore{ GetIndexContentFn: func(repo string) ([]byte, error) { - return nil, ErrTestError + return nil, errMetaTestInjected }, GetRepositoriesFn: func() ([]string, error) { return []string{"repo1", "repo2"}, nil @@ -67,7 +70,7 @@ func TestParseStorageErrors(t *testing.T) { } imageStore2 := mocks.MockedImageStore{ GetRepositoriesFn: func() ([]string, error) { - return nil, ErrTestError + return nil, errMetaTestInjected }, } storeController := storage.StoreController{ @@ -82,7 +85,7 @@ func TestParseStorageErrors(t *testing.T) { }) Convey("metaDB.GetAllRepoNames errors", func() { - metaDB.GetAllRepoNamesFn = func() ([]string, error) { return nil, ErrTestError } + metaDB.GetAllRepoNamesFn = func() ([]string, error) { return nil, errMetaTestInjected } err := meta.ParseStorage(metaDB, storeController, log.NewTestLogger()) So(err, ShouldNotBeNil) @@ -95,7 +98,7 @@ func TestParseStorageErrors(t *testing.T) { storeController := storage.StoreController{DefaultStore: imageStore1} metaDB.GetAllRepoNamesFn = func() ([]string, error) { return []string{"deleted"}, nil } - metaDB.DeleteRepoMetaFn = func(repo string) error { return ErrTestError } + metaDB.DeleteRepoMetaFn = func(repo string) error { return errMetaTestInjected } err := meta.ParseStorage(metaDB, storeController, log.NewTestLogger()) So(err, ShouldNotBeNil) @@ -106,7 +109,7 @@ func TestParseStorageErrors(t *testing.T) { GetRepositoriesFn: func() ([]string, error) { return []string{"repo1", "repo2"}, nil }, } imageStore1.StatIndexFn = func(repo string) (bool, int64, time.Time, error) { - return false, 0, time.Time{}, ErrTestError + return false, 0, time.Time{}, errMetaTestInjected } storeController := storage.StoreController{DefaultStore: imageStore1} @@ -124,7 +127,7 @@ func TestParseStorageErrors(t *testing.T) { Convey("imageStore.GetIndexContent errors", func() { imageStore.GetIndexContentFn = func(repo string) ([]byte, error) { - return nil, ErrTestError + return nil, errMetaTestInjected } err := meta.ParseRepo("repo", metaDB, storeController, log) @@ -144,7 +147,7 @@ func TestParseStorageErrors(t *testing.T) { imageStore.GetIndexContentFn = func(repo string) ([]byte, error) { return []byte("{}"), nil } - metaDB.ResetRepoReferencesFn = func(repo string, tagsToKeep map[string]bool) error { return ErrTestError } + metaDB.ResetRepoReferencesFn = func(repo string, tagsToKeep map[string]bool) error { return errMetaTestInjected } err := meta.ParseRepo("repo", metaDB, storeController, log) So(err, ShouldNotBeNil) }) @@ -184,11 +187,11 @@ func TestParseStorageErrors(t *testing.T) { } imageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) { // Return a non-missing error (not ErrBlobNotFound or PathNotFoundError) - return nil, ErrTestError + return nil, errMetaTestInjected } err := meta.ParseRepo("repo", metaDB, storeController, log) So(err, ShouldNotBeNil) - So(err, ShouldEqual, ErrTestError) + So(err, ShouldEqual, errMetaTestInjected) }) Convey("imageStore.GetImageManifest missing blob - graceful handling", func() { @@ -274,7 +277,7 @@ func TestParseStorageErrors(t *testing.T) { Convey("metaDB.SetRepoReference", func() { metaDB.SetRepoReferenceFn = func(ctx context.Context, repo, reference string, imageMeta mTypes.ImageMeta) error { - return ErrTestError + return errMetaTestInjected } err = meta.ParseRepo("repo", metaDB, storeController, log) @@ -293,7 +296,7 @@ func TestParseStorageErrors(t *testing.T) { Convey("Image Manifest errors", func() { Convey("Get Config blob error", func() { mockImageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) { - return []byte{}, ErrTestError + return []byte{}, errMetaTestInjected } err := meta.SetImageMetaFromInput(ctx, "repo", "tag", ispec.MediaTypeImageManifest, image.Digest(), @@ -326,7 +329,7 @@ func TestParseStorageErrors(t *testing.T) { mockedMetaDB.UpdateSignaturesValidityFn = func(ctx context.Context, repo string, manifestDigest godigest.Digest, ) error { - return ErrTestError + return errMetaTestInjected } err := meta.SetImageMetaFromInput(ctx, "repo", "tag", mediaType, goodNotationSignature.Digest(), goodNotationSignature.ManifestDescriptor.Data, mockImageStore, mockedMetaDB, log) @@ -907,7 +910,7 @@ func TestGetSignatureLayersInfo(t *testing.T) { Convey("GetBlobContent errors", t, func() { mockImageStore := mocks.MockedImageStore{} mockImageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) { - return nil, ErrTestError + return nil, errMetaTestInjected } image := CreateRandomImage() @@ -930,7 +933,7 @@ func TestGetSignatureLayersInfo(t *testing.T) { Convey("notation GetBlobContent errors", t, func() { mockImageStore := mocks.MockedImageStore{} mockImageStore.GetBlobContentFn = func(repo string, digest godigest.Digest) ([]byte, error) { - return nil, ErrTestError + return nil, errMetaTestInjected } image := CreateImageWith().RandomLayers(1, 10).RandomConfig().Build() diff --git a/pkg/regexp/regexp.go b/pkg/regexp/regexp.go index ee30045e..67fedc57 100644 --- a/pkg/regexp/regexp.go +++ b/pkg/regexp/regexp.go @@ -1,10 +1,15 @@ package regexp import ( + "fmt" "regexp" "strings" ) +// TagMaxLen is the maximum length of a manifest tag in the OCI Distribution Specification +// (opencontainers/distribution-spec spec.md, "Pulling manifests"). +const TagMaxLen = 128 + //nolint:gochecknoglobals var ( // alphaNumericRegexp defines the alpha numeric atom, typically a @@ -33,8 +38,20 @@ var ( // FullNameRegexp is the format which matches the full string of the // name component of reference. FullNameRegexp = expression(match("^"), NameRegexp, match("$")) + + // TagRegexp matches a manifest tag per the OCI Distribution Specification + // (opencontainers/distribution-spec spec.md, "Pulling manifests"): a tag MUST be at most + // TagMaxLen characters and MUST match + // [a-zA-Z0-9_][a-zA-Z0-9._-]* with the suffix length bounded by TagMaxLen (anchored). + TagRegexp = match(fmt.Sprintf(`^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,%d}$`, TagMaxLen-1)) ) +// IsDistributionSpecTag reports whether s is a valid distribution-spec tag (same grammar as the +// component in GET /v2//manifests/ when is a tag). +func IsDistributionSpecTag(s string) bool { + return TagRegexp.MatchString(s) +} + // match compiles the string to a regular expression. // //nolint:gochecknoglobals diff --git a/pkg/regexp/regexp_test.go b/pkg/regexp/regexp_test.go new file mode 100644 index 00000000..46418df3 --- /dev/null +++ b/pkg/regexp/regexp_test.go @@ -0,0 +1,41 @@ +package regexp_test + +import ( + "strings" + "testing" + + zreg "zotregistry.dev/zot/v2/pkg/regexp" +) + +func TestIsDistributionSpecTag(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + tag string + valid bool + }{ + {"empty", "", false}, + {"latest", "latest", true}, + {"with dots", "v1.0.0", true}, + {"with hyphen", "meta-a", true}, + {"with underscore", "my_tag", true}, + {"max length", strings.Repeat("a", zreg.TagMaxLen), true}, + {"too long", strings.Repeat("a", zreg.TagMaxLen+1), false}, + {"slash", "bad/ref", false}, + {"leading dot", ".bad", false}, + {"colon", "bad:tag", false}, + {"space", "bad tag", false}, + } + + for _, testCase := range cases { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + got := zreg.IsDistributionSpecTag(testCase.tag) + if got != testCase.valid { + t.Fatalf("IsDistributionSpecTag(%q) = %v, want %v", testCase.tag, got, testCase.valid) + } + }) + } +} diff --git a/pkg/storage/common/common_test.go b/pkg/storage/common/common_test.go index a3c09764..e65c3d1e 100644 --- a/pkg/storage/common/common_test.go +++ b/pkg/storage/common/common_test.go @@ -59,19 +59,19 @@ func TestValidateManifest(t *testing.T) { body, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageConfig, body) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageConfig, body, nil) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrBadManifest) }) Convey("empty manifest with bad media type", func() { - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageConfig, []byte("")) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageConfig, []byte(""), nil) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrBadManifest) }) Convey("empty manifest with correct media type", func() { - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, []byte("")) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, []byte(""), nil) So(err, ShouldNotBeNil) So(err, ShouldEqual, zerr.ErrBadManifest) }) @@ -97,7 +97,7 @@ func TestValidateManifest(t *testing.T) { body, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body, nil) So(err, ShouldNotBeNil) var internalErr *zerr.Error @@ -134,7 +134,7 @@ func TestValidateManifest(t *testing.T) { So(err, ShouldBeNil) // this was actually an umoci error on config blob - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body, nil) So(err, ShouldBeNil) }) @@ -163,7 +163,7 @@ func TestValidateManifest(t *testing.T) { body, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body, nil) So(err, ShouldBeNil) }) @@ -182,7 +182,7 @@ func TestValidateManifest(t *testing.T) { body, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, body, nil) So(err, ShouldBeNil) }) }) diff --git a/pkg/storage/gc/gc_internal_test.go b/pkg/storage/gc/gc_internal_test.go index 30111d4b..04cc30b1 100644 --- a/pkg/storage/gc/gc_internal_test.go +++ b/pkg/storage/gc/gc_internal_test.go @@ -125,7 +125,7 @@ func TestGarbageCollectManifestErrors(t *testing.T) { manifestDigest := godigest.FromBytes(body) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, body) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, body, nil) So(err, ShouldBeNil) Convey("trigger GetIndex error in GetReferencedBlobs", func() { @@ -247,7 +247,7 @@ func TestGarbageCollectIndexErrors(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -264,7 +264,7 @@ func TestGarbageCollectIndexErrors(t *testing.T) { indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) index, err = common.GetIndex(imgStore, repoName, log) diff --git a/pkg/storage/gc/gc_test.go b/pkg/storage/gc/gc_test.go index db722794..c41f08fd 100644 --- a/pkg/storage/gc/gc_test.go +++ b/pkg/storage/gc/gc_test.go @@ -1384,7 +1384,7 @@ func TestGarbageCollectDeletion(t *testing.T) { So(err, ShouldBeNil) rootIndexDigest, _, err := imgStore.PutImageManifest(repoName, "topindex", ispec.MediaTypeImageIndex, - topIndexBlob) + topIndexBlob, nil) So(err, ShouldBeNil) bottomIndex1Digest := bottomIndex1.IndexDescriptor.Digest diff --git a/pkg/storage/gcs/gcs_test.go b/pkg/storage/gcs/gcs_test.go index 8a9b50e3..60cfc421 100644 --- a/pkg/storage/gcs/gcs_test.go +++ b/pkg/storage/gcs/gcs_test.go @@ -492,7 +492,7 @@ func TestGCSDriver(t *testing.T) { // Upload manifest mblob, err := json.Marshal(image.Manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob, nil) So(err, ShouldBeNil) // Verify manifest @@ -540,7 +540,7 @@ func TestGCSDriver(t *testing.T) { // Upload manifest mblob, err := json.Marshal(image.Manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, mblob, nil) So(err, ShouldBeNil) err = imgStore.DeleteImageManifest(repoName, "1.0", false) @@ -638,7 +638,7 @@ func TestGCSDedupe(t *testing.T) { manifestDigest := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe1", manifestDigest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe1", manifestDigest.String()) @@ -712,7 +712,7 @@ func TestGCSDedupe(t *testing.T) { manifestDigest = godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe2", manifestDigest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe2", manifestDigest.String()) @@ -905,7 +905,7 @@ func TestGCSDeleteBlobsInUse(t *testing.T) { manifestBuf, err := json.Marshal(manifest) So(err, ShouldBeNil) - manifestDigest, _, err := imgStore.PutImageManifest("repo", tag, ispec.MediaTypeImageManifest, manifestBuf) + manifestDigest, _, err := imgStore.PutImageManifest("repo", tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) Convey("Try to delete blob currently in use", func() { @@ -1189,19 +1189,19 @@ func TestGCSStorageAPIs(t *testing.T) { Convey("Bad image manifest", func() { _, _, err = imgStore.PutImageManifest("test", digest.String(), "application/json", - manifestBuf) + manifestBuf, nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest, - []byte{}) + []byte{}, nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest, - []byte(`{"test":true}`)) + []byte(`{"test":true}`), nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest, - manifestBuf) + manifestBuf, nil) So(err, ShouldNotBeNil) _, _, _, err = imgStore.GetImageManifest("test", digest.String()) @@ -1250,20 +1250,20 @@ func TestGCSStorageAPIs(t *testing.T) { badMb, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, badMb) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, badMb, nil) So(err, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // same manifest for coverage - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, err = imgStore.GetImageTags("inexistent") @@ -1434,11 +1434,11 @@ func TestGCSStorageAPIs(t *testing.T) { Convey("Bad image manifest", func() { _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, []byte("bad json")) + ispec.MediaTypeImageManifest, []byte("bad json"), nil) So(err, ShouldNotBeNil) _, _, _, err = imgStore.GetImageManifest("test", digest.String()) @@ -1475,12 +1475,12 @@ func TestGCSStorageAPIs(t *testing.T) { digest := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // same manifest for coverage _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("test", digest.String()) @@ -1566,7 +1566,7 @@ func TestGCSStorageAPIs(t *testing.T) { So(err, ShouldBeNil) digest = godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("replace", digest.String()) @@ -1620,7 +1620,7 @@ func TestGCSStorageAPIs(t *testing.T) { So(err, ShouldBeNil) _ = godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) }) @@ -1893,7 +1893,7 @@ func TestGCSMandatoryAnnotations(t *testing.T) { }, }, storeDriver, cacheDriver, nil, nil) - _, _, err = imgStoreWithLinter.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStoreWithLinter.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) }) @@ -1913,7 +1913,7 @@ func TestGCSMandatoryAnnotations(t *testing.T) { }, }, storeDriver, nil, nil, nil) - _, _, err = imgStoreWithLinter.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStoreWithLinter.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) }) }) @@ -1974,7 +1974,7 @@ func pushRandomImageIndexGCS(imgStore storageTypes.ImageStore, repoName string, digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -1991,7 +1991,7 @@ func pushRandomImageIndexGCS(imgStore storageTypes.ImageStore, repoName string, indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) return bdgst, digest, indexDigest, int64(len(indexContent)) @@ -2109,7 +2109,7 @@ func TestGCSGarbageCollectImageManifest(t *testing.T) { digest := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // put artifact referencing above image @@ -2150,7 +2150,7 @@ func TestGCSGarbageCollectImageManifest(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) // push orphan artifact (missing subject) @@ -2168,7 +2168,7 @@ func TestGCSGarbageCollectImageManifest(t *testing.T) { // push orphan artifact manifest _, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) err = garbageCollect.CleanRepo(ctx, repoName) @@ -2315,7 +2315,7 @@ func TestGCSGarbageCollectImageIndex(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst) @@ -2489,7 +2489,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -2525,7 +2525,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) } @@ -2572,7 +2572,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { digest := godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) innerIndex.Manifests = append(innerIndex.Manifests, ispec.Descriptor{ @@ -2590,7 +2590,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { So(innerIndexDigest, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest(repoName, innerIndexDigest.String(), - ispec.MediaTypeImageIndex, innerIndexContent) + ispec.MediaTypeImageIndex, innerIndexContent, nil) So(err, ShouldBeNil) // add inner index into root index @@ -2608,7 +2608,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) artifactManifest := ispec.Manifest{ @@ -2637,7 +2637,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) artifactManifest.Subject = &ispec.Descriptor{ @@ -2654,7 +2654,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest referencing a manifest from index image _, _, err = imgStore.PutImageManifest(repoName, artifactManifestIndexDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestIndexBuf) + ispec.MediaTypeImageManifest, artifactManifestIndexBuf, nil) So(err, ShouldBeNil) artifactManifest.Subject = &ispec.Descriptor{ @@ -2671,7 +2671,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest referencing a manifest from index image _, _, err = imgStore.PutImageManifest(repoName, artifactManifestInnerIndexDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestInnerIndexBuf) + ispec.MediaTypeImageManifest, artifactManifestInnerIndexBuf, nil) So(err, ShouldBeNil) // push artifact manifest pointing to artifact above @@ -2688,7 +2688,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf) _, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) // push orphan artifact (missing subject) @@ -2706,7 +2706,7 @@ func TestGCSGarbageCollectChainedImageIndexes(t *testing.T) { // push orphan artifact manifest _, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst) @@ -3225,7 +3225,7 @@ func RunGCSCheckAllBlobsIntegrityTests( //nolint: thelper indexBlob, err := json.Marshal(index) So(err, ShouldBeNil) - indexDigest, _, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob) + indexDigest, _, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob, nil) So(err, ShouldBeNil) buff := bytes.NewBufferString("") diff --git a/pkg/storage/imagestore/imagestore.go b/pkg/storage/imagestore/imagestore.go index b7a4d3d1..7cba135f 100644 --- a/pkg/storage/imagestore/imagestore.go +++ b/pkg/storage/imagestore/imagestore.go @@ -536,8 +536,11 @@ func (is *ImageStore) GetImageManifest(repo, reference string) ([]byte, godigest } // PutImageManifest adds an image manifest to the repository. -func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //nolint: gocyclo - body []byte, +// When extraTags is non-empty, the reference must be a digest; each entry becomes an +// org.opencontainers.image.ref.name on a separate index descriptor (distribution-spec +// digest push with tag query params). +func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //nolint: gocyclo,cyclop + body []byte, extraTags []string, ) (godigest.Digest, godigest.Digest, error) { if err := is.InitRepo(repo); err != nil { is.log.Debug().Err(err).Msg("init repo") @@ -570,6 +573,12 @@ func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //noli return mDigest, "", err } + // Tag query parameters apply only to digest-addressed pushes (?tag= on PUT .../manifests/). + // If the path reference is not a digest, extraTags must be empty; otherwise the request is invalid. + if len(extraTags) > 0 { + return "", "", zerr.ErrBadManifest + } + refIsDigest = false } @@ -610,27 +619,18 @@ func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //noli artifactType = zcommon.GetManifestArtifactType(manifest) } else if mediaType == ispec.MediaTypeImageIndex { - var index ispec.Index + var imgIndex ispec.Index - err := json.Unmarshal(body, &index) + err := json.Unmarshal(body, &imgIndex) if err != nil { return "", "", err } - if index.Subject != nil { - subjectDigest = index.Subject.Digest + if imgIndex.Subject != nil { + subjectDigest = imgIndex.Subject.Digest } - artifactType = zcommon.GetIndexArtifactType(index) - } - - updateIndex, oldDgst, err := common.CheckIfIndexNeedsUpdate(&index, &desc, is.log) - if err != nil { - return "", "", err - } - - if !updateIndex { - return mDigest, subjectDigest, nil + artifactType = zcommon.GetIndexArtifactType(imgIndex) } // write manifest to "blobs" @@ -647,27 +647,116 @@ func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //noli } } - err = common.UpdateIndexWithPrunedImageManifests(is, &index, repo, desc, oldDgst, is.log) - if err != nil { - return "", "", err - } + var ( + lintDesc ispec.Descriptor + commitEventRefs []string + ) - // now update "index.json" - for midx, manifest := range index.Manifests { - _, ok := manifest.Annotations[ispec.AnnotationRefName] - if !ok && manifest.Digest.String() == desc.Digest.String() { - // matching descriptor does not have a tag, we need to remove it and add the new descriptor - index.Manifests = append(index.Manifests[:midx], index.Manifests[midx+1:]...) + if len(extraTags) > 0 { + for midx := 0; midx < len(index.Manifests); { + manifest := index.Manifests[midx] + _, hasTag := manifest.Annotations[ispec.AnnotationRefName] + if !hasTag && manifest.Digest.String() == mDigest.String() { + index.Manifests = append(index.Manifests[:midx], index.Manifests[midx+1:]...) + + continue + } + + midx++ } + + anyIndexChange := false + + changedTags := make([]string, 0, len(extraTags)) + + var ( + updateIndex bool + oldDgst godigest.Digest + ) + + for _, tag := range extraTags { + descLocal := ispec.Descriptor{ + MediaType: mediaType, + Size: desc.Size, + Digest: mDigest, + Annotations: map[string]string{ + ispec.AnnotationRefName: tag, + }, + } + + updateIndex, oldDgst, err = common.CheckIfIndexNeedsUpdate(&index, &descLocal, is.log) + if err != nil { + return "", "", err + } + + if !updateIndex { + continue + } + + anyIndexChange = true + + if err = common.UpdateIndexWithPrunedImageManifests(is, &index, repo, descLocal, oldDgst, is.log); err != nil { + return "", "", err + } + + index.Manifests = append(index.Manifests, descLocal) + changedTags = append(changedTags, tag) + } + + if !anyIndexChange { + return mDigest, subjectDigest, nil + } + + lintDesc = ispec.Descriptor{ + MediaType: mediaType, + Size: desc.Size, + Digest: mDigest, + ArtifactType: artifactType, + Annotations: map[string]string{ + ispec.AnnotationRefName: changedTags[0], + }, + } + + commitEventRefs = changedTags + } else { + updateIndex, oldDgst, err := common.CheckIfIndexNeedsUpdate(&index, &desc, is.log) + if err != nil { + return "", "", err + } + + if !updateIndex { + return mDigest, subjectDigest, nil + } + + err = common.UpdateIndexWithPrunedImageManifests(is, &index, repo, desc, oldDgst, is.log) + if err != nil { + return "", "", err + } + + // now update "index.json" + for midx := 0; midx < len(index.Manifests); { + manifest := index.Manifests[midx] + _, ok := manifest.Annotations[ispec.AnnotationRefName] + if !ok && manifest.Digest.String() == desc.Digest.String() { + // matching descriptor does not have a tag, we need to remove it and add the new descriptor + index.Manifests = append(index.Manifests[:midx], index.Manifests[midx+1:]...) + + continue + } + + midx++ + } + + index.Manifests = append(index.Manifests, desc) + + // update the descriptors artifact type in order to check for signatures when applying the linter + desc.ArtifactType = artifactType + + lintDesc = desc + commitEventRefs = []string{reference} } - index.Manifests = append(index.Manifests, desc) - - // update the descriptors artifact type in order to check for signatures when applying the linter - desc.ArtifactType = artifactType - - // apply linter only on images, not signatures - pass, err := common.ApplyLinter(is, is.linter, repo, desc) + pass, err := common.ApplyLinter(is, is.linter, repo, lintDesc) if !pass { is.log.Error().Err(err).Str("repository", repo).Str("reference", reference). Msg("linter didn't pass") @@ -684,7 +773,9 @@ func (is *ImageStore) PutImageManifest(repo, reference, mediaType string, //noli } if is.events != nil { - is.events.ImageUpdated(repo, reference, mDigest.String(), mediaType, string(body)) + for _, ref := range commitEventRefs { + is.events.ImageUpdated(repo, ref, mDigest.String(), mediaType, string(body)) + } } return mDigest, subjectDigest, nil diff --git a/pkg/storage/local/local_test.go b/pkg/storage/local/local_test.go index cea78376..3d0fb18b 100644 --- a/pkg/storage/local/local_test.go +++ b/pkg/storage/local/local_test.go @@ -142,7 +142,7 @@ func TestStorageFSAPIs(t *testing.T) { panic(err) } - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) err = os.Chmod(path.Join(imgStore.RootDir(), repoName, "index.json"), 0o755) @@ -150,7 +150,7 @@ func TestStorageFSAPIs(t *testing.T) { panic(err) } - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) manifestPath := path.Join(imgStore.RootDir(), repoName, "blobs", digest.Algorithm().String(), digest.Encoded()) @@ -176,7 +176,7 @@ func TestStorageFSAPIs(t *testing.T) { panic(err) } - _, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) err = os.Chmod(path.Join(imgStore.RootDir(), repoName), 0o755) @@ -371,7 +371,7 @@ func FuzzTestPutGetImageManifest(f *testing.F) { mdigest := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest(repoName, mdigest.String(), ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, mdigest.String(), ispec.MediaTypeImageManifest, manifestBuf, nil) if err != nil && errors.Is(err, zerr.ErrBadManifest) { t.Errorf("the error that occurred is %v \n", err) } @@ -427,7 +427,7 @@ func FuzzTestPutDeleteImageManifest(f *testing.F) { mdigest := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest(repoName, mdigest.String(), ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, mdigest.String(), ispec.MediaTypeImageManifest, manifestBuf, nil) if err != nil && errors.Is(err, zerr.ErrBadManifest) { t.Errorf("the error that occurred is %v \n", err) } @@ -1178,7 +1178,7 @@ func TestDedupeLinks(t *testing.T) { manifestDigest := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe1", manifestDigest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe1", manifestDigest.String()) @@ -1240,7 +1240,7 @@ func TestDedupeLinks(t *testing.T) { So(err, ShouldBeNil) manifestDigest2 := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest("dedupe2", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("dedupe2", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe2", manifestDigest2.String()) @@ -2477,7 +2477,7 @@ func TestGarbageCollectErrors(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -2494,7 +2494,7 @@ func TestGarbageCollectErrors(t *testing.T) { indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) err = os.Chmod(imgStore.BlobPath(repoName, indexDigest), 0o000) @@ -2545,7 +2545,7 @@ func TestGarbageCollectErrors(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) // trigger GetBlobContent error @@ -2604,10 +2604,10 @@ func TestGarbageCollectErrors(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) // upload again same manifest so that we trigger manifest conflict - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) time.Sleep(500 * time.Millisecond) diff --git a/pkg/storage/s3/s3_test.go b/pkg/storage/s3/s3_test.go index 534b1b74..3a71d72c 100644 --- a/pkg/storage/s3/s3_test.go +++ b/pkg/storage/s3/s3_test.go @@ -337,7 +337,7 @@ func TestGetOCIReferrers(t *testing.T) { mbuflen := mbuf.Len() mdigest := godigest.FromBytes(mblob) - d, _, err := imgStore.PutImageManifest(repo, "1.0", ispec.MediaTypeImageManifest, mbuf.Bytes()) + d, _, err := imgStore.PutImageManifest(repo, "1.0", ispec.MediaTypeImageManifest, mbuf.Bytes(), nil) So(d, ShouldEqual, mdigest) So(err, ShouldBeNil) @@ -391,7 +391,7 @@ func TestGetOCIReferrers(t *testing.T) { manBufLen := len(manBuf) manDigest := godigest.FromBytes(manBuf) - _, _, err = imgStore.PutImageManifest(repo, manDigest.Encoded(), ispec.MediaTypeImageManifest, manBuf) + _, _, err = imgStore.PutImageManifest(repo, manDigest.Encoded(), ispec.MediaTypeImageManifest, manBuf, nil) So(err, ShouldBeNil) index, err := imgStore.GetReferrers(repo, mdigest, []string{artifactType}) @@ -665,7 +665,7 @@ func TestNegativeCasesObjectsStorage(t *testing.T) { err = imgStore.DeleteImageManifest(testImage, "1.0", false) So(err, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(testImage, "1.0", "application/json", []byte{}) + _, _, err = imgStore.PutImageManifest(testImage, "1.0", "application/json", []byte{}, nil) So(err, ShouldNotBeNil) _, err = imgStore.PutBlobChunkStreamed(testImage, upload, bytes.NewBufferString(testImage)) @@ -1042,7 +1042,7 @@ func TestS3Dedupe(t *testing.T) { manifestDigest := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe1", manifestDigest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe1", manifestDigest.String()) @@ -1121,7 +1121,7 @@ func TestS3Dedupe(t *testing.T) { manifestDigest2 := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe2", "1.0", ispec.MediaTypeImageManifest, - manifestBuf) + manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe2", manifestDigest2.String()) @@ -1293,7 +1293,7 @@ func TestS3Dedupe(t *testing.T) { manifestDigest3 := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe3", "1.0", ispec.MediaTypeImageManifest, - manifestBuf) + manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe3", manifestDigest3.String()) @@ -1468,7 +1468,7 @@ func TestS3Dedupe(t *testing.T) { manifestDigest := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe1", manifestDigest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe1", manifestDigest.String()) @@ -1538,7 +1538,7 @@ func TestS3Dedupe(t *testing.T) { manifestDigest2 := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe2", "1.0", ispec.MediaTypeImageManifest, - manifestBuf) + manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe2", manifestDigest2.String()) @@ -1763,7 +1763,7 @@ func TestRebuildDedupeIndex(t *testing.T) { digest = godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe1", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe1", digest.String()) @@ -1796,7 +1796,7 @@ func TestRebuildDedupeIndex(t *testing.T) { digest = godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("dedupe2", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("dedupe2", digest.String()) @@ -2893,7 +2893,7 @@ func TestS3ManifestImageIndex(t *testing.T) { So(digest, ShouldNotBeNil) m1content := content - _, _, err = imgStore.PutImageManifest("index", "test:1.0", ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", "test:1.0", ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) // create another manifest but upload using its sha256 reference @@ -2937,7 +2937,7 @@ func TestS3ManifestImageIndex(t *testing.T) { So(digest, ShouldNotBeNil) m2dgst := digest m2size := len(content) - _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) Convey("Image index", func() { @@ -2978,7 +2978,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) var index ispec.Index @@ -3002,7 +3002,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) index1dgst := digest - _, _, err = imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageIndex, content) + _, _, err = imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("index", "test:index1") So(err, ShouldBeNil) @@ -3046,7 +3046,7 @@ func TestS3ManifestImageIndex(t *testing.T) { So(digest, ShouldNotBeNil) m4dgst := digest m4size := len(content) - _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.SchemaVersion = 2 @@ -3069,7 +3069,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", "test:index2", ispec.MediaTypeImageIndex, content) + _, _, err = imgStore.PutImageManifest("index", "test:index2", ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("index", "test:index2") So(err, ShouldBeNil) @@ -3100,7 +3100,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", "test:index3", ispec.MediaTypeImageIndex, content) + _, _, err = imgStore.PutImageManifest("index", "test:index3", ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("index", "test:index3") So(err, ShouldBeNil) @@ -3122,7 +3122,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageIndex, content) + _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("index", digest.String()) So(err, ShouldBeNil) @@ -3202,7 +3202,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("index", digest.String()) So(err, ShouldBeNil) @@ -3222,7 +3222,7 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageIndex, content) + _, _, err = imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("index", "test:index1") So(err, ShouldBeNil) @@ -3278,11 +3278,11 @@ func TestS3ManifestImageIndex(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("index", "test:1.0", ispec.MediaTypeImageIndex, content) + _, _, err = imgStore.PutImageManifest("index", "test:1.0", ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) // previously an image index, try writing a manifest - _, _, err = imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageManifest, m1content) + _, _, err = imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageManifest, m1content, nil) So(err, ShouldBeNil) }) }) @@ -3351,7 +3351,7 @@ func TestS3ManifestImageIndex(t *testing.T) { m1size := len(content) - _, _, err = imgStore.PutImageManifest("index", "test:1.0", ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", "test:1.0", ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) // second config @@ -3386,7 +3386,7 @@ func TestS3ManifestImageIndex(t *testing.T) { So(m2digest, ShouldNotBeNil) m2size := len(content) - _, _, err = imgStore.PutImageManifest("index", m2digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest("index", m2digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) Convey("Put image index with valid subject", func() { @@ -3412,7 +3412,7 @@ func TestS3ManifestImageIndex(t *testing.T) { idigest := godigest.FromBytes(content) So(idigest, ShouldNotBeNil) - digest1, digest2, err := imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageIndex, content) + digest1, digest2, err := imgStore.PutImageManifest("index", "test:index1", ispec.MediaTypeImageIndex, content, nil) So(err, ShouldBeNil) So(digest1.String(), ShouldEqual, idigest.String()) So(digest2.String(), ShouldEqual, m1digest.String()) diff --git a/pkg/storage/scrub_test.go b/pkg/storage/scrub_test.go index ee7f977e..527c34a1 100644 --- a/pkg/storage/scrub_test.go +++ b/pkg/storage/scrub_test.go @@ -490,7 +490,7 @@ func RunCheckAllBlobsIntegrityTests( //nolint: thelper indexBlob, err := json.Marshal(index) So(err, ShouldBeNil) - indexDigest, _, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob) + indexDigest, _, err := imgStore.PutImageManifest(repoName, "", ispec.MediaTypeImageIndex, indexBlob, nil) So(err, ShouldBeNil) buff := bytes.NewBufferString("") diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 735243d1..fc4598c1 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -29,6 +29,7 @@ import ( zerr "zotregistry.dev/zot/v2/errors" "zotregistry.dev/zot/v2/pkg/api/config" rediscfg "zotregistry.dev/zot/v2/pkg/api/config/redis" + "zotregistry.dev/zot/v2/pkg/extensions/events" "zotregistry.dev/zot/v2/pkg/extensions/monitoring" zlog "zotregistry.dev/zot/v2/pkg/log" "zotregistry.dev/zot/v2/pkg/storage" @@ -149,6 +150,31 @@ func createObjectsStore(options createObjectStoreOpts) ( return s3.New(s3Driver), imgStore, cacheDriver, err } +// newLocalImageStoreWithEventRecorder builds a filesystem-backed image store for tests with a non-nil +// events.Recorder. +func newLocalImageStoreWithEventRecorder(t *testing.T, recorder events.Recorder) storageTypes.ImageStore { + t.Helper() + + cacheDir := t.TempDir() + rootDir := t.TempDir() + log := zlog.NewTestLogger() + + cacheDriver, err := storage.Create("boltdb", cache.BoltDBDriverParameters{ + RootDir: cacheDir, + Name: "cache", + UseRelPaths: true, + }, log) + if err != nil { + t.Fatal(err) + } + + storeDriver := local.New(true) + metrics := monitoring.NewMetricsServer(false, log) + + return imagestore.NewImageStore(rootDir, cacheDir, true, true, log, metrics, nil, + storeDriver, cacheDriver, nil, recorder) +} + //nolint:gochecknoglobals var testCases = []struct { testCaseName string @@ -189,6 +215,328 @@ func TestStorageNew(t *testing.T) { }) } +type captureImageEvents struct { + mu sync.Mutex + imageUpdated []imageUpdatedCall +} + +type imageUpdatedCall struct { + repo, reference, digest, mediaType, manifest string +} + +func (c *captureImageEvents) Close() {} + +func (c *captureImageEvents) RepositoryCreated(string) {} + +func (c *captureImageEvents) ImageUpdated(name, reference, digest, mediaType, manifest string) { + c.mu.Lock() + defer c.mu.Unlock() + + c.imageUpdated = append(c.imageUpdated, imageUpdatedCall{ + repo: name, reference: reference, digest: digest, mediaType: mediaType, manifest: manifest, + }) +} + +func (c *captureImageEvents) ImageDeleted(string, string, string, string) {} + +func (c *captureImageEvents) ImageLintFailed(string, string, string, string, string) {} + +// TestPutImageManifestExtraTagsAndEvents covers extra-tag digest pushes, index updates, and ImageUpdated events. +// One Convey uses createObjectsStore (nil recorder); the rest use newLocalImageStoreWithEventRecorder. +func TestPutImageManifestExtraTagsAndEvents(t *testing.T) { + countUntaggedIndexEntriesForDigest := func(t *testing.T, imgStore storageTypes.ImageStore, repo string, + dgst godigest.Digest, + ) int { + t.Helper() + + raw, err := imgStore.GetIndexContent(repo) + So(err, ShouldBeNil) + + var idx ispec.Index + + err = json.Unmarshal(raw, &idx) + So(err, ShouldBeNil) + + n := 0 + + for _, m := range idx.Manifests { + if m.Digest.String() != dgst.String() { + continue + } + + if _, ok := m.Annotations[ispec.AnnotationRefName]; !ok { + n++ + } + } + + return n + } + + // Uses createObjectsStore (nil event recorder); all other Conveys use newLocalImageStoreWithEventRecorder. + Convey("non-digest path reference with extraTags returns ErrBadManifest", t, func() { + cacheDir := t.TempDir() + + opts := createObjectStoreOpts{ + rootDir: cacheDir, + cacheDir: cacheDir, + cacheType: storageConstants.BoltdbName, + storageType: storageConstants.LocalStorageDriverName, + } + + _, imgStore, _, err := createObjectsStore(opts) + So(err, ShouldBeNil) + + repo := "badtagquery" + + cblob, cdigest := GetRandomImageConfig() + _, _, err = imgStore.FullBlobUpload(repo, bytes.NewReader(cblob), cdigest) + So(err, ShouldBeNil) + + layerBytes := []byte("layer") + layerDigest := godigest.FromBytes(layerBytes) + _, _, err = imgStore.FullBlobUpload(repo, bytes.NewReader(layerBytes), layerDigest) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + manifest.SchemaVersion = 2 + manifest.Config = ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + } + manifest.Layers = []ispec.Descriptor{ + {MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: layerDigest, Size: int64(len(layerBytes))}, + } + + manifestBuf, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + _, _, err = imgStore.PutImageManifest(repo, "1.0", ispec.MediaTypeImageManifest, manifestBuf, + []string{"extra"}) + So(errors.Is(err, zerr.ErrBadManifest), ShouldBeTrue) + }) + + Convey("digest push with multiple extra tags applies tags, emits ImageUpdated per tag, idempotent replay", t, func() { + eventCapture := &captureImageEvents{} + imgStore := newLocalImageStoreWithEventRecorder(t, eventCapture) + + repo := "mquery" + + cblob, cdigest := GetRandomImageConfig() + _, _, err := imgStore.FullBlobUpload(repo, bytes.NewReader(cblob), cdigest) + So(err, ShouldBeNil) + + layerBytes := []byte("layer-bytes") + layerDigest := godigest.FromBytes(layerBytes) + _, _, err = imgStore.FullBlobUpload(repo, bytes.NewReader(layerBytes), layerDigest) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + manifest.SchemaVersion = 2 + manifest.Config = ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + } + manifest.Layers = []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.layer.v1.tar", + Digest: layerDigest, + Size: int64(len(layerBytes)), + }, + } + + manifestBuf, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + manifestDigest := godigest.FromBytes(manifestBuf) + extraTags := []string{"v1.2.3", "v1.2", "latest"} + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, extraTags) + So(err, ShouldBeNil) + + tags, err := imgStore.GetImageTags(repo) + So(err, ShouldBeNil) + + for _, qt := range extraTags { + So(slices.Contains(tags, qt), ShouldBeTrue) + } + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 3) + + wantRefs := map[string]struct{}{"v1.2.3": {}, "v1.2": {}, "latest": {}} + + for _, imageUpd := range eventCapture.imageUpdated { + So(imageUpd.repo, ShouldEqual, repo) + So(imageUpd.digest, ShouldEqual, manifestDigest.String()) + So(imageUpd.mediaType, ShouldEqual, ispec.MediaTypeImageManifest) + So(imageUpd.manifest, ShouldEqual, string(manifestBuf)) + _, ok := wantRefs[imageUpd.reference] + So(ok, ShouldBeTrue) + + delete(wantRefs, imageUpd.reference) + } + + So(len(wantRefs), ShouldEqual, 0) + eventCapture.mu.Unlock() + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, extraTags) + So(err, ShouldBeNil) + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 3) + eventCapture.mu.Unlock() + }) + + Convey("digest push then digest+tags removes untagged index row for that digest", t, func() { + eventCapture := &captureImageEvents{} + imgStore := newLocalImageStoreWithEventRecorder(t, eventCapture) + + repo := "strip-untagged" + + cblob, cdigest := GetRandomImageConfig() + _, _, err := imgStore.FullBlobUpload(repo, bytes.NewReader(cblob), cdigest) + So(err, ShouldBeNil) + + layerBytes := []byte("layer-strip") + layerDigest := godigest.FromBytes(layerBytes) + _, _, err = imgStore.FullBlobUpload(repo, bytes.NewReader(layerBytes), layerDigest) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + manifest.SchemaVersion = 2 + manifest.Config = ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + } + manifest.Layers = []ispec.Descriptor{ + {MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: layerDigest, Size: int64(len(layerBytes))}, + } + + manifestBuf, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + manifestDigest := godigest.FromBytes(manifestBuf) + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, nil) + So(err, ShouldBeNil) + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 1) + So(eventCapture.imageUpdated[0].repo, ShouldEqual, repo) + So(eventCapture.imageUpdated[0].reference, ShouldEqual, manifestDigest.String()) + So(eventCapture.imageUpdated[0].digest, ShouldEqual, manifestDigest.String()) + eventCapture.mu.Unlock() + + So(countUntaggedIndexEntriesForDigest(t, imgStore, repo, manifestDigest), ShouldEqual, 1) + + extraTags := []string{"after-tag"} + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, extraTags) + So(err, ShouldBeNil) + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 2) + So(eventCapture.imageUpdated[1].reference, ShouldEqual, "after-tag") + So(eventCapture.imageUpdated[1].digest, ShouldEqual, manifestDigest.String()) + eventCapture.mu.Unlock() + + So(countUntaggedIndexEntriesForDigest(t, imgStore, repo, manifestDigest), ShouldEqual, 0) + + tags, err := imgStore.GetImageTags(repo) + So(err, ShouldBeNil) + So(slices.Contains(tags, "after-tag"), ShouldBeTrue) + }) + + Convey("digest push with tag subset is no-op for index and events; new tag adds one event", t, func() { + eventCapture := &captureImageEvents{} + imgStore := newLocalImageStoreWithEventRecorder(t, eventCapture) + + repo := "tag-subset" + + cblob, cdigest := GetRandomImageConfig() + _, _, err := imgStore.FullBlobUpload(repo, bytes.NewReader(cblob), cdigest) + So(err, ShouldBeNil) + + layerBytes := []byte("layer-subset") + layerDigest := godigest.FromBytes(layerBytes) + _, _, err = imgStore.FullBlobUpload(repo, bytes.NewReader(layerBytes), layerDigest) + So(err, ShouldBeNil) + + manifest := ispec.Manifest{} + manifest.SchemaVersion = 2 + manifest.Config = ispec.Descriptor{ + MediaType: "application/vnd.oci.image.config.v1+json", + Digest: cdigest, + Size: int64(len(cblob)), + } + manifest.Layers = []ispec.Descriptor{ + {MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: layerDigest, Size: int64(len(layerBytes))}, + } + + manifestBuf, err := json.Marshal(manifest) + So(err, ShouldBeNil) + + manifestDigest := godigest.FromBytes(manifestBuf) + firstTags := []string{"tag-a", "tag-b", "tag-c"} + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, firstTags) + So(err, ShouldBeNil) + + tags, err := imgStore.GetImageTags(repo) + So(err, ShouldBeNil) + + for _, qt := range firstTags { + So(slices.Contains(tags, qt), ShouldBeTrue) + } + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 3) + eventCapture.mu.Unlock() + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, []string{"tag-b"}) + So(err, ShouldBeNil) + + tags, err = imgStore.GetImageTags(repo) + So(err, ShouldBeNil) + + for _, qt := range firstTags { + So(slices.Contains(tags, qt), ShouldBeTrue) + } + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 3) + eventCapture.mu.Unlock() + + _, _, err = imgStore.PutImageManifest(repo, manifestDigest.String(), + ispec.MediaTypeImageManifest, manifestBuf, []string{"tag-d"}) + So(err, ShouldBeNil) + + tags, err = imgStore.GetImageTags(repo) + So(err, ShouldBeNil) + So(slices.Contains(tags, "tag-d"), ShouldBeTrue) + + for _, qt := range firstTags { + So(slices.Contains(tags, qt), ShouldBeTrue) + } + + eventCapture.mu.Lock() + So(len(eventCapture.imageUpdated), ShouldEqual, 4) + So(eventCapture.imageUpdated[3].reference, ShouldEqual, "tag-d") + So(eventCapture.imageUpdated[3].digest, ShouldEqual, manifestDigest.String()) + So(eventCapture.imageUpdated[3].manifest, ShouldEqual, string(manifestBuf)) + eventCapture.mu.Unlock() + }) +} + func TestGetAllDedupeReposCandidates(t *testing.T) { for _, testcase := range testCases { t.Run(testcase.testCaseName, func(t *testing.T) { @@ -532,19 +880,19 @@ func TestStorageAPIs(t *testing.T) { Convey("Bad image manifest", func() { _, _, err = imgStore.PutImageManifest("test", digest.String(), "application/json", - manifestBuf) + manifestBuf, nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest, - []byte{}) + []byte{}, nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest, - []byte(`{"test":true}`)) + []byte(`{"test":true}`), nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), ispec.MediaTypeImageManifest, - manifestBuf) + manifestBuf, nil) So(err, ShouldNotBeNil) _, _, _, err = imgStore.GetImageManifest("test", digest.String()) @@ -593,20 +941,20 @@ func TestStorageAPIs(t *testing.T) { badMb, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, badMb) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, badMb, nil) So(err, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // same manifest for coverage - _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "2.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "3.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, err = imgStore.GetImageTags("inexistent") @@ -777,11 +1125,11 @@ func TestStorageAPIs(t *testing.T) { Convey("Bad image manifest", func() { _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, []byte("bad json")) + ispec.MediaTypeImageManifest, []byte("bad json"), nil) So(err, ShouldNotBeNil) _, _, _, err = imgStore.GetImageManifest("test", digest.String()) @@ -818,12 +1166,12 @@ func TestStorageAPIs(t *testing.T) { digest := godigest.FromBytes(manifestBuf) _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // same manifest for coverage _, _, err = imgStore.PutImageManifest("test", digest.String(), - ispec.MediaTypeImageManifest, manifestBuf) + ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("test", digest.String()) @@ -918,7 +1266,7 @@ func TestStorageAPIs(t *testing.T) { So(err, ShouldBeNil) digest = godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) _, _, _, err = imgStore.GetImageManifest("replace", digest.String()) @@ -972,7 +1320,7 @@ func TestStorageAPIs(t *testing.T) { So(err, ShouldBeNil) _ = godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("replace", "1.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) }) @@ -1109,7 +1457,7 @@ func TestMandatoryAnnotations(t *testing.T) { So(err, ShouldBeNil) Convey("Missing mandatory annotations", func() { - _, _, err = imgStore.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) }) @@ -1137,7 +1485,7 @@ func TestMandatoryAnnotations(t *testing.T) { }, store, cacheDriver, nil, nil) } - _, _, err = imgStore.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest("test", "1.0.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldNotBeNil) }) }) @@ -1299,7 +1647,7 @@ func TestDeleteBlobsInUse(t *testing.T) { manifestBuf, err := json.Marshal(manifest) So(err, ShouldBeNil) - manifestDigest, _, err := imgStore.PutImageManifest("repo", tag, ispec.MediaTypeImageManifest, manifestBuf) + manifestDigest, _, err := imgStore.PutImageManifest("repo", tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) Convey("Try to delete blob currently in use", func() { @@ -1425,7 +1773,7 @@ func TestDeleteBlobsInUse(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -1442,7 +1790,8 @@ func TestDeleteBlobsInUse(t *testing.T) { indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - indexManifestDigest, _, err := imgStore.PutImageManifest(repoName, "index", ispec.MediaTypeImageIndex, indexContent) + indexManifestDigest, _, err := imgStore.PutImageManifest(repoName, "index", + ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) Convey("Try to delete manifest being referenced by image index", func() { @@ -1883,7 +2232,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { digest := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) err = gc.CleanRepo(ctx, repoName) @@ -1927,7 +2276,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) err = gc.CleanRepo(ctx, repoName) @@ -2066,7 +2415,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { digest := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // put artifact referencing above image @@ -2107,7 +2456,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) // push artifact manifest pointing to artifact above @@ -2123,7 +2472,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf) _, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) // push orphan artifact (missing subject) @@ -2141,7 +2490,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { // push orphan artifact manifest _, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) err = gc.CleanRepo(ctx, repoName) @@ -2198,7 +2547,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { Convey("Garbage collect - don't gc manifests/blobs which are referenced by another image", func() { // upload same image with another tag - _, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repoName, "2.0", ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) err = imgStore.DeleteImageManifest(repoName, tag, false) @@ -2317,7 +2666,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { manifestBuf, err := json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest(repo1Name, tag, ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repo1Name, tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) // sleep so past GC timeout @@ -2381,7 +2730,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { manifestBuf, err = json.Marshal(manifest) So(err, ShouldBeNil) - _, _, err = imgStore.PutImageManifest(repo2Name, tag, ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repo2Name, tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) hasBlob, _, err = imgStore.CheckBlob(repo2Name, bdigest) @@ -2440,7 +2789,7 @@ func TestGarbageCollectImageManifest(t *testing.T) { digest := godigest.FromBytes(manifestBuf) - _, _, err = imgStore.PutImageManifest(repo2Name, tag, ispec.MediaTypeImageManifest, manifestBuf) + _, _, err = imgStore.PutImageManifest(repo2Name, tag, ispec.MediaTypeImageManifest, manifestBuf, nil) So(err, ShouldBeNil) err = gc.CleanRepo(ctx, repo2Name) @@ -2554,7 +2903,7 @@ func TestGarbageCollectImageIndex(t *testing.T) { // push artifact manifest referencing index image _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) artifactManifest.Subject = &ispec.Descriptor{ @@ -2570,7 +2919,7 @@ func TestGarbageCollectImageIndex(t *testing.T) { // push artifact manifest referencing a manifest from index image _, _, err = imgStore.PutImageManifest(repoName, artifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) err = gc.CleanRepo(ctx, repoName) @@ -2701,7 +3050,7 @@ func TestGarbageCollectImageIndex(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) artifactManifest.Subject = &ispec.Descriptor{ @@ -2718,7 +3067,7 @@ func TestGarbageCollectImageIndex(t *testing.T) { // push artifact manifest referencing a manifest from index image _, _, err = imgStore.PutImageManifest(repoName, artifactManifestIndexDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestIndexBuf) + ispec.MediaTypeImageManifest, artifactManifestIndexBuf, nil) So(err, ShouldBeNil) // push artifact manifest pointing to artifact above @@ -2734,7 +3083,7 @@ func TestGarbageCollectImageIndex(t *testing.T) { artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf) _, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) // push orphan artifact (missing subject) @@ -2752,7 +3101,7 @@ func TestGarbageCollectImageIndex(t *testing.T) { // push orphan artifact manifest _, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst) @@ -3015,7 +3364,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -3051,7 +3400,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) } @@ -3098,7 +3447,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { digest := godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) innerIndex.Manifests = append(innerIndex.Manifests, ispec.Descriptor{ @@ -3116,7 +3465,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { So(innerIndexDigest, ShouldNotBeNil) _, _, err = imgStore.PutImageManifest(repoName, innerIndexDigest.String(), - ispec.MediaTypeImageIndex, innerIndexContent) + ispec.MediaTypeImageIndex, innerIndexContent, nil) So(err, ShouldBeNil) // add inner index into root index @@ -3134,7 +3483,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) artifactManifest := ispec.Manifest{ @@ -3163,7 +3512,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest _, _, err = imgStore.PutImageManifest(repoName, artifactDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) artifactManifest.Subject = &ispec.Descriptor{ @@ -3180,7 +3529,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest referencing a manifest from index image _, _, err = imgStore.PutImageManifest(repoName, artifactManifestIndexDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestIndexBuf) + ispec.MediaTypeImageManifest, artifactManifestIndexBuf, nil) So(err, ShouldBeNil) artifactManifest.Subject = &ispec.Descriptor{ @@ -3197,7 +3546,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { // push artifact manifest referencing a manifest from index image _, _, err = imgStore.PutImageManifest(repoName, artifactManifestInnerIndexDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestInnerIndexBuf) + ispec.MediaTypeImageManifest, artifactManifestInnerIndexBuf, nil) So(err, ShouldBeNil) // push artifact manifest pointing to artifact above @@ -3214,7 +3563,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { artifactOfArtifactManifestDigest := godigest.FromBytes(artifactManifestBuf) _, _, err = imgStore.PutImageManifest(repoName, artifactOfArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) // push orphan artifact (missing subject) @@ -3232,7 +3581,7 @@ func TestGarbageCollectChainedImageIndexes(t *testing.T) { // push orphan artifact manifest _, _, err = imgStore.PutImageManifest(repoName, orphanArtifactManifestDigest.String(), - ispec.MediaTypeImageManifest, artifactManifestBuf) + ispec.MediaTypeImageManifest, artifactManifestBuf, nil) So(err, ShouldBeNil) hasBlob, _, err := imgStore.CheckBlob(repoName, bdgst) @@ -3404,7 +3753,7 @@ func pushRandomImageIndex(imgStore storageTypes.ImageStore, repoName string, digest = godigest.FromBytes(content) So(digest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content) + _, _, err = imgStore.PutImageManifest(repoName, digest.String(), ispec.MediaTypeImageManifest, content, nil) So(err, ShouldBeNil) index.Manifests = append(index.Manifests, ispec.Descriptor{ @@ -3421,7 +3770,7 @@ func pushRandomImageIndex(imgStore storageTypes.ImageStore, repoName string, indexDigest := godigest.FromBytes(indexContent) So(indexDigest, ShouldNotBeNil) - _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent) + _, _, err = imgStore.PutImageManifest(repoName, "1.0", ispec.MediaTypeImageIndex, indexContent, nil) So(err, ShouldBeNil) return bdgst, digest, indexDigest, int64(len(indexContent)) diff --git a/pkg/storage/types/types.go b/pkg/storage/types/types.go index 237d8e40..589153fa 100644 --- a/pkg/storage/types/types.go +++ b/pkg/storage/types/types.go @@ -35,7 +35,8 @@ type ImageStore interface { //nolint:interfacebloat GetNextRepositories(repo string, maxEntries int, fn FilterRepoFunc) ([]string, bool, error) GetImageTags(repo string) ([]string, error) GetImageManifest(repo, reference string) ([]byte, godigest.Digest, string, error) - PutImageManifest(repo, reference, mediaType string, body []byte) (godigest.Digest, godigest.Digest, error) + PutImageManifest(repo, reference, mediaType string, body []byte, extraTags []string) ( + godigest.Digest, godigest.Digest, error) DeleteImageManifest(repo, reference string, detectCollision bool) error BlobUploadPath(repo, uuid string) string StatBlobUpload(repo, uuid string) (bool, int64, time.Time, error) diff --git a/pkg/test/image-utils/write.go b/pkg/test/image-utils/write.go index 827aaa8a..47979fef 100644 --- a/pkg/test/image-utils/write.go +++ b/pkg/test/image-utils/write.go @@ -51,7 +51,7 @@ func WriteImageToFileSystem(image Image, repoName, ref string, storeController s return err } - _, _, err = store.PutImageManifest(repoName, ref, image.Manifest.MediaType, manifestBlob) + _, _, err = store.PutImageManifest(repoName, ref, image.Manifest.MediaType, manifestBlob, nil) if err != nil { return err } @@ -82,7 +82,7 @@ func WriteMultiArchImageToFileSystem(multiarchImage MultiarchImage, repoName, re } _, _, err = store.PutImageManifest(repoName, ref, multiarchImage.Index.MediaType, - indexBlob) + indexBlob, nil) return err } diff --git a/pkg/test/image-utils/write_test.go b/pkg/test/image-utils/write_test.go index 4c048c56..9598cf64 100644 --- a/pkg/test/image-utils/write_test.go +++ b/pkg/test/image-utils/write_test.go @@ -67,7 +67,7 @@ func TestWriteImageToFileSystem(t *testing.T) { "tag", storage.StoreController{ DefaultStore: mocks.MockedImageStore{ - PutImageManifestFn: func(repo, reference, mediaType string, body []byte, + PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string, ) (godigest.Digest, godigest.Digest, error) { return "", "", ErrTestError }, diff --git a/pkg/test/mocks/image_store_mock.go b/pkg/test/mocks/image_store_mock.go index c5150deb..77de2508 100644 --- a/pkg/test/mocks/image_store_mock.go +++ b/pkg/test/mocks/image_store_mock.go @@ -23,8 +23,8 @@ type MockedImageStore struct { GetNextRepositoriesFn func(lastRepo string, maxEntries int, fn storageTypes.FilterRepoFunc) ([]string, bool, error) GetImageTagsFn func(repo string) ([]string, error) GetImageManifestFn func(repo string, reference string) ([]byte, godigest.Digest, string, error) - PutImageManifestFn func(repo string, reference string, mediaType string, body []byte) (godigest.Digest, - godigest.Digest, error) + PutImageManifestFn func(repo string, reference string, mediaType string, body []byte, + extraTags []string) (godigest.Digest, godigest.Digest, error) DeleteImageManifestFn func(repo string, reference string, detectCollision bool) error BlobUploadPathFn func(repo string, uuid string) string StatBlobUploadFn func(repo string, uuid string) (bool, int64, time.Time, error) @@ -163,9 +163,10 @@ func (is MockedImageStore) PutImageManifest( reference string, mediaType string, body []byte, + extraTags []string, ) (godigest.Digest, godigest.Digest, error) { if is.PutImageManifestFn != nil { - return is.PutImageManifestFn(repo, reference, mediaType, body) + return is.PutImageManifestFn(repo, reference, mediaType, body, extraTags) } return "", "", nil diff --git a/swagger/docs.go b/swagger/docs.go index 72cb6db2..fb2c45c3 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -31,7 +31,7 @@ const docTemplate = `{ "summary": "Check API support", "responses": { "200": { - "description": "ok\".", + "description": "ok", "schema": { "type": "string" } @@ -114,13 +114,13 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -157,7 +157,7 @@ const docTemplate = `{ } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -200,13 +200,13 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -300,9 +300,6 @@ const docTemplate = `{ "responses": { "202": { "description": "accepted", - "schema": { - "type": "string" - }, "headers": { "Location": { "type": "string", @@ -364,6 +361,19 @@ const docTemplate = `{ "responses": { "204": { "description": "no content", + "headers": { + "Location": { + "type": "string", + "description": "/v2/{name}/blobs/uploads/{session_id}" + }, + "Range": { + "type": "string", + "description": "0-128" + } + } + }, + "400": { + "description": "bad request", "schema": { "type": "string" } @@ -417,8 +427,15 @@ const docTemplate = `{ "responses": { "201": { "description": "created", - "schema": { - "type": "string" + "headers": { + "Docker-Content-Digest": { + "type": "string", + "description": "Digest of the committed blob" + }, + "Location": { + "type": "string", + "description": "/v2/{name}/blobs/{digest}" + } } }, "404": { @@ -461,11 +478,8 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } + "204": { + "description": "no content" }, "404": { "description": "not found", @@ -509,10 +523,11 @@ const docTemplate = `{ "responses": { "202": { "description": "accepted", - "schema": { - "type": "string" - }, "headers": { + "Blob-Upload-UUID": { + "type": "string", + "description": "Opaque blob upload session identifier" + }, "Location": { "type": "string", "description": "/v2/{name}/blobs/uploads/{session_id}" @@ -612,10 +627,7 @@ const docTemplate = `{ ], "responses": { "202": { - "description": "accepted", - "schema": { - "type": "string" - } + "description": "accepted" } } }, @@ -651,8 +663,9 @@ const docTemplate = `{ "$ref": "#/definitions/api.ImageManifest" }, "headers": { - "constants.DistContentDigestKey": { - "type": "object" + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the content" } } } @@ -692,8 +705,9 @@ const docTemplate = `{ "$ref": "#/definitions/api.ImageManifest" }, "headers": { - "constants.DistContentDigestKey": { - "type": "object" + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the content" } } }, @@ -712,7 +726,7 @@ const docTemplate = `{ } }, "put": { - "description": "Update an image's manifest given a reference or a digest", + "description": "Update an image's manifest given a reference or a digest. On digest pushes with ` + "`" + `tag=` + "`" + ` query\nparameters, 201 responses repeat the ` + "`" + `OCI-Tag` + "`" + ` header once per tag value.", "consumes": [ "application/json" ], @@ -734,13 +748,30 @@ const docTemplate = `{ "name": "reference", "in": "path", "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "additional tag(s) for digest pushes", + "name": "tag", + "in": "query" } ], "responses": { "201": { "description": "created", - "schema": { - "type": "string" + "headers": { + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the uploaded content" + }, + "OCI-Tag": { + "type": "string", + "description": "Echoed tag= value; this header is repeatable (one field per tag= query parameter)" + } } }, "400": { @@ -755,6 +786,12 @@ const docTemplate = `{ "type": "string" } }, + "414": { + "description": "too many tag query parameters", + "schema": { + "type": "string" + } + }, "500": { "description": "internal server error", "schema": { @@ -789,8 +826,35 @@ const docTemplate = `{ } ], "responses": { - "200": { - "description": "ok", + "202": { + "description": "accepted" + }, + "400": { + "description": "bad request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "not found", + "schema": { + "type": "string" + } + }, + "405": { + "description": "method not allowed", + "schema": { + "type": "string" + } + }, + "409": { + "description": "conflict", + "schema": { + "type": "string" + } + }, + "500": { + "description": "internal server error", "schema": { "type": "string" } @@ -829,8 +893,9 @@ const docTemplate = `{ "type": "string" }, "headers": { - "constants.DistContentDigestKey": { - "type": "object" + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the content" } } }, @@ -841,7 +906,7 @@ const docTemplate = `{ } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -944,7 +1009,7 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } @@ -1094,13 +1159,13 @@ const docTemplate = `{ "summary": "Logout by removing current session", "responses": { "200": { - "description": "ok\".", + "description": "ok", "schema": { "type": "string" } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } diff --git a/swagger/swagger.json b/swagger/swagger.json index eacb1457..247f95fa 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -23,7 +23,7 @@ "summary": "Check API support", "responses": { "200": { - "description": "ok\".", + "description": "ok", "schema": { "type": "string" } @@ -106,13 +106,13 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -149,7 +149,7 @@ } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -192,13 +192,13 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -292,9 +292,6 @@ "responses": { "202": { "description": "accepted", - "schema": { - "type": "string" - }, "headers": { "Location": { "type": "string", @@ -356,6 +353,19 @@ "responses": { "204": { "description": "no content", + "headers": { + "Location": { + "type": "string", + "description": "/v2/{name}/blobs/uploads/{session_id}" + }, + "Range": { + "type": "string", + "description": "0-128" + } + } + }, + "400": { + "description": "bad request", "schema": { "type": "string" } @@ -409,8 +419,15 @@ "responses": { "201": { "description": "created", - "schema": { - "type": "string" + "headers": { + "Docker-Content-Digest": { + "type": "string", + "description": "Digest of the committed blob" + }, + "Location": { + "type": "string", + "description": "/v2/{name}/blobs/{digest}" + } } }, "404": { @@ -453,11 +470,8 @@ } ], "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } + "204": { + "description": "no content" }, "404": { "description": "not found", @@ -501,10 +515,11 @@ "responses": { "202": { "description": "accepted", - "schema": { - "type": "string" - }, "headers": { + "Blob-Upload-UUID": { + "type": "string", + "description": "Opaque blob upload session identifier" + }, "Location": { "type": "string", "description": "/v2/{name}/blobs/uploads/{session_id}" @@ -604,10 +619,7 @@ ], "responses": { "202": { - "description": "accepted", - "schema": { - "type": "string" - } + "description": "accepted" } } }, @@ -643,8 +655,9 @@ "$ref": "#/definitions/api.ImageManifest" }, "headers": { - "constants.DistContentDigestKey": { - "type": "object" + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the content" } } } @@ -684,8 +697,9 @@ "$ref": "#/definitions/api.ImageManifest" }, "headers": { - "constants.DistContentDigestKey": { - "type": "object" + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the content" } } }, @@ -704,7 +718,7 @@ } }, "put": { - "description": "Update an image's manifest given a reference or a digest", + "description": "Update an image's manifest given a reference or a digest. On digest pushes with `tag=` query\nparameters, 201 responses repeat the `OCI-Tag` header once per tag value.", "consumes": [ "application/json" ], @@ -726,13 +740,30 @@ "name": "reference", "in": "path", "required": true + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "description": "additional tag(s) for digest pushes", + "name": "tag", + "in": "query" } ], "responses": { "201": { "description": "created", - "schema": { - "type": "string" + "headers": { + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the uploaded content" + }, + "OCI-Tag": { + "type": "string", + "description": "Echoed tag= value; this header is repeatable (one field per tag= query parameter)" + } } }, "400": { @@ -747,6 +778,12 @@ "type": "string" } }, + "414": { + "description": "too many tag query parameters", + "schema": { + "type": "string" + } + }, "500": { "description": "internal server error", "schema": { @@ -781,8 +818,35 @@ } ], "responses": { - "200": { - "description": "ok", + "202": { + "description": "accepted" + }, + "400": { + "description": "bad request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "not found", + "schema": { + "type": "string" + } + }, + "405": { + "description": "method not allowed", + "schema": { + "type": "string" + } + }, + "409": { + "description": "conflict", + "schema": { + "type": "string" + } + }, + "500": { + "description": "internal server error", "schema": { "type": "string" } @@ -821,8 +885,9 @@ "type": "string" }, "headers": { - "constants.DistContentDigestKey": { - "type": "object" + "Docker-Content-Digest": { + "type": "string", + "description": "Manifest digest of the content" } } }, @@ -833,7 +898,7 @@ } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } @@ -936,7 +1001,7 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } @@ -1086,13 +1151,13 @@ "summary": "Logout by removing current session", "responses": { "200": { - "description": "ok\".", + "description": "ok", "schema": { "type": "string" } }, "500": { - "description": "internal server error\".", + "description": "internal server error", "schema": { "type": "string" } diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index a60d42ff..09b30dcc 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -256,7 +256,7 @@ paths: - application/json responses: "200": - description: ok". + description: ok schema: type: string summary: Check API support @@ -310,11 +310,11 @@ paths: schema: type: string "400": - description: bad request". + description: bad request schema: type: string "500": - description: internal server error". + description: internal server error schema: type: string summary: Upload cosign public keys for verifying signatures @@ -338,7 +338,7 @@ paths: schema: $ref: '#/definitions/extensions.StrippedConfig' "500": - description: internal server error". + description: internal server error schema: type: string summary: Get current server configuration @@ -366,11 +366,11 @@ paths: schema: type: string "400": - description: bad request". + description: bad request schema: type: string "500": - description: internal server error". + description: internal server error schema: type: string summary: Upload notation certificates for verifying signatures @@ -438,8 +438,6 @@ paths: responses: "202": description: accepted - schema: - type: string summary: Delete image blob/layer get: consumes: @@ -485,8 +483,9 @@ paths: "200": description: OK headers: - constants.DistContentDigestKey: - type: object + Docker-Content-Digest: + description: Manifest digest of the content + type: string schema: $ref: '#/definitions/api.ImageManifest' summary: Check image blob/layer @@ -513,8 +512,6 @@ paths: Range: description: 0-0 type: string - schema: - type: string "401": description: unauthorized schema: @@ -547,10 +544,8 @@ paths: produces: - application/json responses: - "200": - description: ok - schema: - type: string + "204": + description: no content "404": description: not found schema: @@ -580,6 +575,15 @@ paths: responses: "204": description: no content + headers: + Location: + description: /v2/{name}/blobs/uploads/{session_id} + type: string + Range: + description: 0-128 + type: string + "400": + description: bad request schema: type: string "404": @@ -612,14 +616,15 @@ paths: "202": description: accepted headers: + Blob-Upload-UUID: + description: Opaque blob upload session identifier + type: string Location: description: /v2/{name}/blobs/uploads/{session_id} type: string Range: description: 0-128 type: string - schema: - type: string "400": description: bad request schema: @@ -662,8 +667,13 @@ paths: responses: "201": description: created - schema: - type: string + headers: + Docker-Content-Digest: + description: Digest of the committed blob + type: string + Location: + description: /v2/{name}/blobs/{digest} + type: string "404": description: not found schema: @@ -692,8 +702,26 @@ paths: produces: - application/json responses: - "200": - description: ok + "202": + description: accepted + "400": + description: bad request + schema: + type: string + "404": + description: not found + schema: + type: string + "405": + description: method not allowed + schema: + type: string + "409": + description: conflict + schema: + type: string + "500": + description: internal server error schema: type: string summary: Delete image manifest @@ -718,8 +746,9 @@ paths: "200": description: OK headers: - constants.DistContentDigestKey: - type: object + Docker-Content-Digest: + description: Manifest digest of the content + type: string schema: $ref: '#/definitions/api.ImageManifest' "404": @@ -752,8 +781,9 @@ paths: "200": description: ok headers: - constants.DistContentDigestKey: - type: object + Docker-Content-Digest: + description: Manifest digest of the content + type: string schema: type: string "404": @@ -761,14 +791,16 @@ paths: schema: type: string "500": - description: internal server error". + description: internal server error schema: type: string summary: Check image manifest put: consumes: - application/json - description: Update an image's manifest given a reference or a digest + description: |- + Update an image's manifest given a reference or a digest. On digest pushes with `tag=` query + parameters, 201 responses repeat the `OCI-Tag` header once per tag value. parameters: - description: repository name in: path @@ -780,13 +812,26 @@ paths: name: reference required: true type: string + - collectionFormat: multi + description: additional tag(s) for digest pushes + in: query + items: + type: string + name: tag + type: array produces: - application/json responses: "201": description: created - schema: - type: string + headers: + Docker-Content-Digest: + description: Manifest digest of the uploaded content + type: string + OCI-Tag: + description: Echoed tag= value; this header is repeatable (one field + per tag= query parameter) + type: string "400": description: bad request schema: @@ -795,6 +840,10 @@ paths: description: not found schema: type: string + "414": + description: too many tag query parameters + schema: + type: string "500": description: internal server error schema: @@ -865,7 +914,7 @@ paths: schema: $ref: '#/definitions/common.ImageTags' "400": - description: bad request". + description: bad request schema: type: string "404": @@ -965,11 +1014,11 @@ paths: - application/json responses: "200": - description: ok". + description: ok schema: type: string "500": - description: internal server error". + description: internal server error schema: type: string summary: Logout by removing current session