forked from TrueCloudLab/distribution
Merge pull request #3143 from eyJhb/pagination
OCI: Add pagination on `/v2/<name>/tags/list`
This commit is contained in:
commit
d80a63f1ea
5 changed files with 75 additions and 0 deletions
|
@ -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.
|
`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_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.
|
`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.
|
`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.
|
`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.
|
`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.
|
||||||
|
|
|
@ -490,6 +490,18 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Failures: []ResponseDescriptor{
|
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,
|
unauthorizedResponseDescriptor,
|
||||||
repositoryNotFoundResponseDescriptor,
|
repositoryNotFoundResponseDescriptor,
|
||||||
deniedResponseDescriptor,
|
deniedResponseDescriptor,
|
||||||
|
|
|
@ -144,4 +144,14 @@ var (
|
||||||
longer proceed.`,
|
longer proceed.`,
|
||||||
HTTPStatusCode: http.StatusNotFound,
|
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,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,8 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
"github.com/distribution/distribution/v3/registry/api/errcode"
|
"github.com/distribution/distribution/v3/registry/api/errcode"
|
||||||
|
@ -49,6 +51,51 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package storage
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
"path"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
|
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)
|
tags = append(tags, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// there is no guarantee for the order,
|
||||||
|
// therefore sort before return.
|
||||||
|
sort.Strings(tags)
|
||||||
|
|
||||||
return tags, nil
|
return tags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue