From febc8733d2c4fce9d9d50105d8ccd6ce001c9444 Mon Sep 17 00:00:00 2001 From: "eyjhbb@gmail.com" Date: Tue, 5 May 2020 11:28:31 +0200 Subject: [PATCH 1/3] added error codes for pagination Signed-off-by: eyjhb --- registry/api/v2/descriptors.go | 12 ++++++++++++ registry/api/v2/errors.go | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/registry/api/v2/descriptors.go b/registry/api/v2/descriptors.go index 56bfdcdf..1dbe6823 100644 --- a/registry/api/v2/descriptors.go +++ b/registry/api/v2/descriptors.go @@ -490,6 +490,18 @@ var routeDescriptors = []RouteDescriptor{ }, }, Failures: []ResponseDescriptor{ + { + Name: "Invalid pagination number", + Description: "The received parameter n was invalid in some way, as described by the error code. The client should resolve the issue and retry the request.", + StatusCode: http.StatusBadRequest, + Body: BodyDescriptor{ + ContentType: "application/json", + Format: errorsBody, + }, + ErrorCodes: []errcode.ErrorCode{ + ErrorCodePaginationNumberInvalid, + }, + }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, diff --git a/registry/api/v2/errors.go b/registry/api/v2/errors.go index c413efbb..ac68af48 100644 --- a/registry/api/v2/errors.go +++ b/registry/api/v2/errors.go @@ -133,4 +133,14 @@ var ( longer proceed.`, HTTPStatusCode: http.StatusNotFound, }) + + // ErrorCodePaginationNumberInvalid is returned when the `n` parameter is + // not an integer, or `n` is negative. + ErrorCodePaginationNumberInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ + Value: "PAGINATION_NUMBER_INVALID", + Message: "invalid number of results requested", + Description: `Returned when the "n" parameter (number of results + to return) is not an integer, or "n" is negative.`, + HTTPStatusCode: http.StatusBadRequest, + }) ) From 4da2712b527a170d15ed4c6e7bd63c3d47ff3cf3 Mon Sep 17 00:00:00 2001 From: eyjhb Date: Tue, 30 Jun 2020 08:45:56 +0200 Subject: [PATCH 2/3] added pagination to `v2//tags/list` endpoint Signed-off-by: eyjhb --- registry/handlers/tags.go | 47 ++++++++++++++++++++++++++++++++++++ registry/storage/tagstore.go | 5 ++++ 2 files changed, 52 insertions(+) diff --git a/registry/handlers/tags.go b/registry/handlers/tags.go index 72d1dc8f..0cc38102 100644 --- a/registry/handlers/tags.go +++ b/registry/handlers/tags.go @@ -3,6 +3,8 @@ package handlers import ( "encoding/json" "net/http" + "sort" + "strconv" "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/registry/api/errcode" @@ -49,6 +51,51 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { return } + // do pagination if requested + q := r.URL.Query() + // get entries after latest, if any specified + if lastEntry := q.Get("last"); lastEntry != "" { + lastEntryIndex := sort.SearchStrings(tags, lastEntry) + + // as`sort.SearchStrings` can return len(tags), if the + // specified `lastEntry` is not found, we need to + // ensure it does not panic when slicing. + if lastEntryIndex == len(tags) { + tags = []string{} + } else { + tags = tags[lastEntryIndex+1:] + } + } + + // if no error, means that the user requested `n` entries + if n := q.Get("n"); n != "" { + maxEntries, err := strconv.Atoi(n) + if err != nil || maxEntries < 0 { + th.Errors = append(th.Errors, v2.ErrorCodePaginationNumberInvalid.WithDetail(map[string]string{"n": n})) + return + } + + // if there is requested more than or + // equal to the amount of tags we have, + // then set the request to equal `len(tags)`. + // the reason for the `=`, is so the else + // clause will only activate if there + // are tags left the user needs. + if maxEntries >= len(tags) { + maxEntries = len(tags) + } else if maxEntries > 0 { + // defined in `catalog.go` + urlStr, err := createLinkEntry(r.URL.String(), maxEntries, tags[maxEntries-1]) + if err != nil { + th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) + return + } + w.Header().Set("Link", urlStr) + } + + tags = tags[:maxEntries] + } + w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) diff --git a/registry/storage/tagstore.go b/registry/storage/tagstore.go index e33bf9da..7bbb2f01 100644 --- a/registry/storage/tagstore.go +++ b/registry/storage/tagstore.go @@ -3,6 +3,7 @@ package storage import ( "context" "path" + "sort" "github.com/distribution/distribution/v3" storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" @@ -47,6 +48,10 @@ func (ts *tagStore) All(ctx context.Context) ([]string, error) { tags = append(tags, filename) } + // there is no guarantee for the order, + // therefore sort before return. + sort.Strings(tags) + return tags, nil } From 9cf39997afbfde59cd04801b8fd1dcd37144ed3d Mon Sep 17 00:00:00 2001 From: eyjhb Date: Fri, 23 Apr 2021 18:04:25 +0200 Subject: [PATCH 3/3] added pagination error to api docs Signed-off-by: eyjhb --- docs/spec/api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/spec/api.md b/docs/spec/api.md index 747b4f81..1f7a4a7b 100644 --- a/docs/spec/api.md +++ b/docs/spec/api.md @@ -1142,6 +1142,7 @@ The error codes encountered via the API are enumerated in the following table: `MANIFEST_UNVERIFIED` | manifest failed signature verification | During manifest upload, if the manifest fails signature verification, this error will be returned. `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. + `PAGINATION_NUMBER_INVALID` | invalid number of results requested | Returned when the `n` parameter (number of results to return) is not an integer, or `n` is negative. `SIZE_INVALID` | provided length did not match content length | When a layer is uploaded, the provided size will be checked against the uploaded content. If they do not match, this error will be returned. `TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned. `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate.