mirror of
https://github.com/distribution/distribution.git
synced 2025-06-22 05:31:00 +00:00
Merge 21050e0288
into da404778ed
This commit is contained in:
commit
d8f25540bd
@ -350,7 +350,11 @@ func (t *tags) Lookup(ctx context.Context, digest v1.Descriptor) ([]string, erro
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (t *tags) Tag(ctx context.Context, tag string, desc v1.Descriptor) error {
|
||||
func (t *tags) List(ctx context.Context, limit int, last string) ([]string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
|
@ -564,9 +564,11 @@ func TestTagsAPI(t *testing.T) {
|
||||
queryParams: url.Values{"last": []string{"does-not-exist"}, "n": []string{"3"}},
|
||||
expectedStatusCode: http.StatusOK,
|
||||
expectedBody: tagsAPIResponse{Name: imageName.Name(), Tags: []string{
|
||||
"jyi7b",
|
||||
"kb0j5",
|
||||
"sb71y",
|
||||
}},
|
||||
expectedLinkHeader: `</v2/test/tags/list?last=sb71y&n=3>; rel="next"`,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,8 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
@ -34,9 +34,34 @@ type tagsAPIResponse struct {
|
||||
|
||||
// GetTags returns a json list of tags for a specific image name.
|
||||
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||
var moreEntries = true
|
||||
|
||||
q := r.URL.Query()
|
||||
lastEntry := q.Get("last")
|
||||
|
||||
limit := -1
|
||||
|
||||
// parse n, if n unparseable, or negative assign it to defaultReturnedEntries
|
||||
if n := q.Get("n"); n != "" {
|
||||
parsedMax, err := strconv.Atoi(n)
|
||||
if err != nil || parsedMax < 0 {
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodePaginationNumberInvalid.WithDetail(map[string]int{"n": parsedMax}))
|
||||
return
|
||||
|
||||
}
|
||||
limit = parsedMax
|
||||
}
|
||||
|
||||
filled := make([]string, 0)
|
||||
|
||||
if limit == 0 {
|
||||
moreEntries = false
|
||||
} else {
|
||||
tagService := th.Repository.Tags(th)
|
||||
tags, err := tagService.All(th)
|
||||
// if limit is -1, we want to list all the tags, and receive a io.EOF error
|
||||
returnedTags, err := tagService.List(th.Context, limit, lastEntry)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
switch err := err.(type) {
|
||||
case distribution.ErrRepositoryUnknown:
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodeNameUnknown.WithDetail(map[string]string{"name": th.Repository.Named().Name()}))
|
||||
@ -47,42 +72,18 @@ 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:]
|
||||
// err is either io.EOF
|
||||
moreEntries = false
|
||||
}
|
||||
filled = returnedTags
|
||||
}
|
||||
|
||||
// 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, errcode.ErrorCodePaginationNumberInvalid.WithDetail(map[string]string{"n": n}))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// 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])
|
||||
// Add a link header if there are more entries to retrieve
|
||||
if moreEntries {
|
||||
lastEntry = filled[len(filled)-1]
|
||||
urlStr, err := createLinkEntry(r.URL.String(), limit, lastEntry)
|
||||
if err != nil {
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
@ -90,15 +91,10 @@ func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Link", urlStr)
|
||||
}
|
||||
|
||||
tags = tags[:maxEntries]
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
if err := enc.Encode(tagsAPIResponse{
|
||||
Name: th.Repository.Named().Name(),
|
||||
Tags: tags,
|
||||
Tags: filled,
|
||||
}); err != nil {
|
||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||
return
|
||||
|
@ -65,3 +65,7 @@ func (pt proxyTagService) All(ctx context.Context) ([]string, error) {
|
||||
func (pt proxyTagService) Lookup(ctx context.Context, digest v1.Descriptor) ([]string, error) {
|
||||
return []string{}, distribution.ErrUnsupported
|
||||
}
|
||||
|
||||
func (pt proxyTagService) List(ctx context.Context, limit int, last string) ([]string, error) {
|
||||
return []string{}, distribution.ErrUnsupported
|
||||
}
|
||||
|
@ -58,7 +58,14 @@ func (m *mockTagStore) All(ctx context.Context) ([]string, error) {
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func testProxyTagService(local, remote map[string]v1.Descriptor) *proxyTagService {
|
||||
func (m *mockTagStore) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (m *mockTagStore) List(ctx context.Context, limit int, last string) ([]string, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func testProxyTagService(local, remote map[string]distribution.Descriptor) *proxyTagService {
|
||||
if local == nil {
|
||||
local = make(map[string]v1.Descriptor)
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/opencontainers/go-digest"
|
||||
@ -230,3 +233,85 @@ func (ts *tagStore) ManifestDigests(ctx context.Context, tag string) ([]digest.D
|
||||
}
|
||||
return dgsts, nil
|
||||
}
|
||||
|
||||
// List returns the tags for the repository.
|
||||
func (ts *tagStore) List(ctx context.Context, limit int, last string) ([]string, error) {
|
||||
filledBuffer := false
|
||||
foundTags := 0
|
||||
var tags []string
|
||||
|
||||
if limit == 0 {
|
||||
return tags, errors.New("attempted to list 0 tags")
|
||||
}
|
||||
|
||||
root, err := pathFor(manifestTagsPathSpec{
|
||||
name: ts.repository.Named().Name(),
|
||||
})
|
||||
if err != nil {
|
||||
return tags, err
|
||||
}
|
||||
|
||||
startAfter := ""
|
||||
if last != "" {
|
||||
startAfter, err = pathFor(manifestTagPathSpec{
|
||||
name: ts.repository.Named().Name(),
|
||||
tag: last,
|
||||
})
|
||||
if err != nil {
|
||||
return tags, err
|
||||
}
|
||||
}
|
||||
|
||||
err = ts.blobStore.driver.Walk(ctx, root, func(fileInfo storagedriver.FileInfo) error {
|
||||
return handleTag(fileInfo, root, last, func(tagPath string) error {
|
||||
tags = append(tags, tagPath)
|
||||
foundTags += 1
|
||||
// if we've filled our slice, no need to walk any further
|
||||
if limit > 0 && foundTags == limit {
|
||||
filledBuffer = true
|
||||
return storagedriver.ErrFilledBuffer
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}, storagedriver.WithStartAfterHint(startAfter))
|
||||
|
||||
if err != nil {
|
||||
switch err := err.(type) {
|
||||
case storagedriver.PathNotFoundError:
|
||||
return tags, distribution.ErrRepositoryUnknown{Name: ts.repository.Named().Name()}
|
||||
default:
|
||||
return tags, err
|
||||
}
|
||||
}
|
||||
|
||||
if filledBuffer {
|
||||
// There are potentially more tags to list
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// We didn't fill the buffer, so that's the end of the list of tags
|
||||
return tags, io.EOF
|
||||
}
|
||||
|
||||
// handleTag calls function fn with a tag path if fileInfo
|
||||
// has a path of a tag under root and that it is lexographically
|
||||
// after last. Otherwise, it will return ErrSkipDir or ErrFilledBuffer.
|
||||
// These should be used with Walk to do handling with repositories in a
|
||||
// storage.
|
||||
func handleTag(fileInfo storagedriver.FileInfo, root, last string, fn func(tagPath string) error) error {
|
||||
filePath := fileInfo.Path()
|
||||
|
||||
// lop the base path off
|
||||
tag := filePath[len(root)+1:]
|
||||
parts := strings.SplitN(tag, "/", 2)
|
||||
if len(parts) > 1 {
|
||||
return storagedriver.ErrSkipDir
|
||||
}
|
||||
|
||||
if lessPath(last, tag) {
|
||||
if err := fn(tag); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return storagedriver.ErrSkipDir
|
||||
}
|
||||
|
3
tags.go
3
tags.go
@ -27,6 +27,9 @@ type TagService interface {
|
||||
|
||||
// Lookup returns the set of tags referencing the given digest.
|
||||
Lookup(ctx context.Context, digest v1.Descriptor) ([]string, error)
|
||||
|
||||
// List returns the set of tags after last managed by this tag service
|
||||
List(ctx context.Context, limit int, last string) ([]string, error)
|
||||
}
|
||||
|
||||
// TagManifestsProvider provides method to retrieve the digests of manifests that a tag historically
|
||||
|
Loading…
Reference in New Issue
Block a user