mirror of
https://github.com/project-zot/zot.git
synced 2026-06-19 14:08:01 +08:00
feat: support pushing multiple tags for a single manifest (#3885)
* feat: support pushing multiple tags for a single manifest See https://github.com/opencontainers/distribution-spec/pull/600 Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> * fix: constants not replaced in swagger output Also godot mandates comments ending in dots, which produces bad results in the swagger generated files, see the extra ". which is now fixed below: ``` diff --git a/swagger/docs.go b/swagger/docs.go index 84b08277..fb2c45c3 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -114,7 +114,7 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } @@ -200,7 +200,7 @@ const docTemplate = `{ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } diff --git a/swagger/swagger.json b/swagger/swagger.json index cfeb3900..247f95fa 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -106,7 +106,7 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } @@ -192,7 +192,7 @@ } }, "400": { - "description": "bad request\".", + "description": "bad request", "schema": { "type": "string" } diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 57641c2f..09b30dcc 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -310,7 +310,7 @@ paths: schema: type: string "400": - description: bad request". + description: bad request schema: type: string "500": @@ -366,7 +366,7 @@ paths: schema: type: string "400": - description: bad request". + description: bad request schema: type: string "500": ``` Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com> --------- Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
This commit is contained in:
@@ -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/<digest>?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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+116
-30
@@ -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
|
||||
|
||||
+101
-5
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user