diff --git a/internal/client/repository.go b/internal/client/repository.go index 4f8659306..fab172b79 100644 --- a/internal/client/repository.go +++ b/internal/client/repository.go @@ -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") } diff --git a/registry/handlers/api_test.go b/registry/handlers/api_test.go index 754def6c4..02fe35ada 100644 --- a/registry/handlers/api_test.go +++ b/registry/handlers/api_test.go @@ -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: `; rel="next"`, }, } diff --git a/registry/handlers/tags.go b/registry/handlers/tags.go index bfd56fc62..3c9c9c916 100644 --- a/registry/handlers/tags.go +++ b/registry/handlers/tags.go @@ -2,8 +2,8 @@ package handlers import ( "encoding/json" + "io" "net/http" - "sort" "strconv" "github.com/distribution/distribution/v3" @@ -34,71 +34,67 @@ 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) { - tagService := th.Repository.Tags(th) - 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 - } + var moreEntries = true - // 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) + 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 - // 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 - 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 - } + filled := make([]string, 0) - // 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)) + if limit == 0 { + moreEntries = false + } else { + tagService := th.Repository.Tags(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()})) + case errcode.Error: + th.Errors = append(th.Errors, err) + default: + th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) + } return } - w.Header().Set("Link", urlStr) + // err is either io.EOF + moreEntries = false } - - tags = tags[:maxEntries] + filled = returnedTags } 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) 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 diff --git a/registry/proxy/proxytagservice.go b/registry/proxy/proxytagservice.go index a55f22bb1..ec39c8a83 100644 --- a/registry/proxy/proxytagservice.go +++ b/registry/proxy/proxytagservice.go @@ -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 +} diff --git a/registry/proxy/proxytagservice_test.go b/registry/proxy/proxytagservice_test.go index b297488ae..55e2f83b6 100644 --- a/registry/proxy/proxytagservice_test.go +++ b/registry/proxy/proxytagservice_test.go @@ -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) } diff --git a/registry/storage/tagstore.go b/registry/storage/tagstore.go index 3639e2d9c..74afe862e 100644 --- a/registry/storage/tagstore.go +++ b/registry/storage/tagstore.go @@ -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 +} diff --git a/tags.go b/tags.go index ed94a51a0..670f0ac2d 100644 --- a/tags.go +++ b/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