From 60de6e344351b111bcddae02f04d107aa29cee8c Mon Sep 17 00:00:00 2001 From: njucjc Date: Thu, 9 Apr 2026 20:25:58 +0800 Subject: [PATCH] fix(proxy): fix tag list endpoint in proxy mode Signed-off-by: njucjc Signed-off-by: chenjinci.cjc --- internal/client/repository.go | 61 ++++++++++++++++++- internal/client/repository_test.go | 82 ++++++++++++++++++++++++++ registry/proxy/proxytagservice.go | 10 +++- registry/proxy/proxytagservice_test.go | 46 ++++++++++++++- 4 files changed, 194 insertions(+), 5 deletions(-) diff --git a/internal/client/repository.go b/internal/client/repository.go index 71c8c6b7a..3d1451a98 100644 --- a/internal/client/repository.go +++ b/internal/client/repository.go @@ -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 { diff --git a/internal/client/repository_test.go b/internal/client/repository_test.go index 50206e21b..2feec8738 100644 --- a/internal/client/repository_test.go +++ b/internal/client/repository_test.go @@ -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) diff --git a/registry/proxy/proxytagservice.go b/registry/proxy/proxytagservice.go index ec39c8a83..e126a9194 100644 --- a/registry/proxy/proxytagservice.go +++ b/registry/proxy/proxytagservice.go @@ -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) } diff --git a/registry/proxy/proxytagservice_test.go b/registry/proxy/proxytagservice_test.go index 55e2f83b6..eb328e9db 100644 --- a/registry/proxy/proxytagservice_test.go +++ b/registry/proxy/proxytagservice_test.go @@ -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) } }