mirror of
https://github.com/distribution/distribution.git
synced 2025-06-22 21:50:56 +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")
|
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")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,9 +564,11 @@ func TestTagsAPI(t *testing.T) {
|
|||||||
queryParams: url.Values{"last": []string{"does-not-exist"}, "n": []string{"3"}},
|
queryParams: url.Values{"last": []string{"does-not-exist"}, "n": []string{"3"}},
|
||||||
expectedStatusCode: http.StatusOK,
|
expectedStatusCode: http.StatusOK,
|
||||||
expectedBody: tagsAPIResponse{Name: imageName.Name(), Tags: []string{
|
expectedBody: tagsAPIResponse{Name: imageName.Name(), Tags: []string{
|
||||||
|
"jyi7b",
|
||||||
"kb0j5",
|
"kb0j5",
|
||||||
"sb71y",
|
"sb71y",
|
||||||
}},
|
}},
|
||||||
|
expectedLinkHeader: `</v2/test/tags/list?last=sb71y&n=3>; rel="next"`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,8 +2,8 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
@ -34,71 +34,67 @@ type tagsAPIResponse struct {
|
|||||||
|
|
||||||
// GetTags returns a json list of tags for a specific image name.
|
// GetTags returns a json list of tags for a specific image name.
|
||||||
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) {
|
||||||
tagService := th.Repository.Tags(th)
|
var moreEntries = true
|
||||||
tags, err := tagService.All(th)
|
|
||||||
if err != nil {
|
|
||||||
switch err := err.(type) {
|
|
||||||
case distribution.ErrRepositoryUnknown:
|
|
||||||
th.Errors = append(th.Errors, errcode.ErrorCodeNameUnknown.WithDetail(map[string]string{"name": th.Repository.Named().Name()}))
|
|
||||||
case errcode.Error:
|
|
||||||
th.Errors = append(th.Errors, err)
|
|
||||||
default:
|
|
||||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// do pagination if requested
|
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
// get entries after latest, if any specified
|
lastEntry := q.Get("last")
|
||||||
if lastEntry := q.Get("last"); lastEntry != "" {
|
|
||||||
lastEntryIndex := sort.SearchStrings(tags, lastEntry)
|
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
|
||||||
|
|
||||||
// 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:]
|
|
||||||
}
|
}
|
||||||
|
limit = parsedMax
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no error, means that the user requested `n` entries
|
filled := make([]string, 0)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is requested more than or
|
if limit == 0 {
|
||||||
// equal to the amount of tags we have,
|
moreEntries = false
|
||||||
// then set the request to equal `len(tags)`.
|
} else {
|
||||||
// the reason for the `=`, is so the else
|
tagService := th.Repository.Tags(th)
|
||||||
// clause will only activate if there
|
// if limit is -1, we want to list all the tags, and receive a io.EOF error
|
||||||
// are tags left the user needs.
|
returnedTags, err := tagService.List(th.Context, limit, lastEntry)
|
||||||
if maxEntries >= len(tags) {
|
if err != nil {
|
||||||
maxEntries = len(tags)
|
if err != io.EOF {
|
||||||
} else if maxEntries > 0 {
|
switch err := err.(type) {
|
||||||
// defined in `catalog.go`
|
case distribution.ErrRepositoryUnknown:
|
||||||
urlStr, err := createLinkEntry(r.URL.String(), maxEntries, tags[maxEntries-1])
|
th.Errors = append(th.Errors, errcode.ErrorCodeNameUnknown.WithDetail(map[string]string{"name": th.Repository.Named().Name()}))
|
||||||
if err != nil {
|
case errcode.Error:
|
||||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
th.Errors = append(th.Errors, err)
|
||||||
|
default:
|
||||||
|
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Link", urlStr)
|
// err is either io.EOF
|
||||||
|
moreEntries = false
|
||||||
}
|
}
|
||||||
|
filled = returnedTags
|
||||||
tags = tags[:maxEntries]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
w.Header().Set("Link", urlStr)
|
||||||
|
}
|
||||||
|
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
if err := enc.Encode(tagsAPIResponse{
|
if err := enc.Encode(tagsAPIResponse{
|
||||||
Name: th.Repository.Named().Name(),
|
Name: th.Repository.Named().Name(),
|
||||||
Tags: tags,
|
Tags: filled,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
|
||||||
return
|
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) {
|
func (pt proxyTagService) Lookup(ctx context.Context, digest v1.Descriptor) ([]string, error) {
|
||||||
return []string{}, distribution.ErrUnsupported
|
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
|
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 {
|
if local == nil {
|
||||||
local = make(map[string]v1.Descriptor)
|
local = make(map[string]v1.Descriptor)
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,11 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
@ -230,3 +233,85 @@ func (ts *tagStore) ManifestDigests(ctx context.Context, tag string) ([]digest.D
|
|||||||
}
|
}
|
||||||
return dgsts, nil
|
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 returns the set of tags referencing the given digest.
|
||||||
Lookup(ctx context.Context, digest v1.Descriptor) ([]string, error)
|
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
|
// TagManifestsProvider provides method to retrieve the digests of manifests that a tag historically
|
||||||
|
Loading…
Reference in New Issue
Block a user