fix(proxy): fix tag list endpoint in proxy mode

Signed-off-by: njucjc <njucjc@alibaba-inc.com>
Signed-off-by: chenjinci.cjc <chenjinci.cjc@alibaba-inc.com>
This commit is contained in:
njucjc
2026-04-09 20:25:58 +08:00
committed by chenjinci.cjc
parent 708f8d6b06
commit 60de6e3443
4 changed files with 194 additions and 5 deletions

View File

@@ -345,7 +345,66 @@ func (t *tags) Lookup(ctx context.Context, digest v1.Descriptor) ([]string, erro
}
func (t *tags) List(ctx context.Context, limit int, last string) ([]string, error) {
panic("not implemented")
if limit < 0 {
tags, err := t.All(ctx)
if err != nil {
return tags, err
}
// return io.EOF, indicating that there are no more tags to list
return tags, io.EOF
}
v := url.Values{}
v.Add("n", strconv.Itoa(limit))
if last != "" {
v.Add("last", last)
}
listURLStr, err := t.ub.BuildTagsURL(t.name, v)
if err != nil {
return nil, err
}
listURL, err := url.Parse(listURLStr)
if err != nil {
return nil, err
}
preAlloc := 1000
if limit < preAlloc {
preAlloc = limit
}
tags := make([]string, 0, preAlloc)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, listURL.String(), nil)
if err != nil {
return nil, err
}
resp, err := t.client.Do(req)
if err != nil {
return tags, err
}
defer resp.Body.Close()
if err := HandleHTTPResponseError(resp); err != nil {
return tags, err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return tags, err
}
tagsResponse := struct {
Tags []string `json:"tags"`
}{}
if err := json.Unmarshal(b, &tagsResponse); err != nil {
return tags, err
}
tags = append(tags, tagsResponse.Tags...)
// if there is a Link header, return nil to indicate that there are more tags to list
// otherwise return io.EOF to indicate that there are no more tags to list
if link := resp.Header.Get("Link"); link != "" {
return tags, nil
}
return tags, io.EOF
}
func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {

View File

@@ -1660,6 +1660,88 @@ func TestManifestTagsPaginated(t *testing.T) {
}
}
func TestManifestTagsListPaginated(t *testing.T) {
s := httptest.NewServer(http.NotFoundHandler())
defer s.Close()
repo, _ := reference.WithName("test.example.com/repo/tags/list")
tagsList := []string{"tag1", "tag2", "funtag"}
var m testutil.RequestResponseMap
for i := range 3 {
body, err := json.Marshal(map[string]any{
"name": "test.example.com/repo/tags/list",
"tags": []string{tagsList[i]},
})
if err != nil {
t.Fatal(err)
}
queryParams := make(map[string][]string)
if i > 0 {
queryParams["n"] = []string{"1"}
queryParams["last"] = []string{tagsList[i-1]}
} else {
queryParams["n"] = []string{"1"}
}
// Test both relative and absolute links.
relativeLink := "/v2/" + repo.Name() + "/tags/list?n=1&last=" + tagsList[i]
var link string
switch i {
case 0:
link = relativeLink
case len(tagsList) - 1:
link = ""
default:
link = s.URL + relativeLink
}
headers := http.Header(map[string][]string{
"Content-Length": {fmt.Sprint(len(body))},
"Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)},
})
if link != "" {
headers.Set("Link", fmt.Sprintf(`<%s>; rel="next"`, link))
}
m = append(m, testutil.RequestResponseMapping{
Request: testutil.Request{
Method: http.MethodGet,
Route: "/v2/" + repo.Name() + "/tags/list",
QueryParams: queryParams,
},
Response: testutil.Response{
StatusCode: http.StatusOK,
Body: body,
Headers: headers,
},
})
}
s.Config.Handler = testutil.NewHandler(m)
r, err := NewRepository(repo, s.URL, nil)
if err != nil {
t.Fatal(err)
}
ctx := dcontext.Background()
tagService := r.Tags(ctx)
last := ""
for i := range 3 {
tags, err := tagService.List(ctx, 1, last)
if err != nil && err != io.EOF {
t.Fatal(err)
}
if len(tags) != 1 {
t.Fatalf("Wrong number of tags returned: %d, expected 1", len(tags))
}
if tags[0] != tagsList[i] {
t.Fatalf("Wrong tag returned: %s, expected %s", tags[0], tagsList[i])
}
last = tags[0]
}
}
func TestManifestUnauthorized(t *testing.T) {
repo, _ := reference.WithName("test.example.com/repo")
_, dgst, _ := newRandomOCIManifest(t, 6)

View File

@@ -2,6 +2,7 @@ package proxy
import (
"context"
"io"
"github.com/distribution/distribution/v3"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
@@ -67,5 +68,12 @@ func (pt proxyTagService) Lookup(ctx context.Context, digest v1.Descriptor) ([]s
}
func (pt proxyTagService) List(ctx context.Context, limit int, last string) ([]string, error) {
return []string{}, distribution.ErrUnsupported
err := pt.authChallenger.tryEstablishChallenges(ctx)
if err == nil {
tags, err := pt.remoteTags.List(ctx, limit, last)
if err == nil || err == io.EOF {
return tags, err
}
}
return pt.localTags.List(ctx, limit, last)
}

View File

@@ -63,8 +63,22 @@ func (m *mockTagStore) Lookup(ctx context.Context, digest distribution.Descripto
}
func (m *mockTagStore) List(ctx context.Context, limit int, last string) ([]string, error) {
panic("not implemented")
m.Lock()
defer m.Unlock()
tags := make([]string, 0, len(m.mapping))
for tag := range m.mapping {
tags = append(tags, tag)
}
sort.Strings(tags)
result := make([]string, 0, limit)
for _, tag := range tags {
if tag > last && len(result) < limit {
result = append(result, tag)
}
}
return result, nil
}
func testProxyTagService(local, remote map[string]distribution.Descriptor) *proxyTagService {
if local == nil {
local = make(map[string]v1.Descriptor)
@@ -179,7 +193,33 @@ func TestGet(t *testing.T) {
t.Fatalf("Unexpected tags returned from All() : %v ", all)
}
if proxyTags.authChallenger.(*mockChallenger).count != 4 {
t.Fatalf("Expected 4 auth challenge calls, got %#v", proxyTags.authChallenger)
list, err := proxyTags.List(ctx, 1, "")
if err != nil {
t.Fatal(err)
}
if len(list) != 1 {
t.Fatalf("Unexpected tag length returned from List() : %d ", len(list))
}
if list[0] != "funtag" {
t.Fatalf("Unexpected tags returned from List() : %v ", list)
}
list2, err := proxyTags.List(ctx, 1, "funtag")
if err != nil {
t.Fatal(err)
}
if len(list2) != 1 {
t.Fatalf("Unexpected tag length returned from List() : %d ", len(list2))
}
if list2[0] != "remote" {
t.Fatalf("Unexpected tags returned from List() : %v ", list2)
}
if proxyTags.authChallenger.(*mockChallenger).count != 6 {
t.Fatalf("Expected 6 auth challenge calls, got %#v", proxyTags.authChallenger)
}
}