Skip to content

Commit fdd1276

Browse files
rmalpani-uberevelynl94
authored andcommitted
Support for pagination in list catalog (#201)
+ Tagserver now sends list and listRepositories in paginated format + TagClient support list and listRepositories with pagination + registryoverride support pagination for catalog listing + Test cases
1 parent 63cd7ff commit fdd1276

File tree

7 files changed

+329
-68
lines changed

7 files changed

+329
-68
lines changed

build-index/tagclient/client.go

Lines changed: 104 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import (
1919
"encoding/json"
2020
"errors"
2121
"fmt"
22+
"io"
2223
"io/ioutil"
2324
"net/url"
25+
"strconv"
2426
"time"
2527

28+
"github.com/uber/kraken/build-index/tagmodels"
2629
"github.com/uber/kraken/core"
2730
"github.com/uber/kraken/lib/healthcheck"
2831
"github.com/uber/kraken/utils/httputil"
@@ -40,7 +43,9 @@ type Client interface {
4043
Get(tag string) (core.Digest, error)
4144
Has(tag string) (bool, error)
4245
List(prefix string) ([]string, error)
46+
ListWithPagination(prefix string, filter ListFilter) (tagmodels.ListResponse, error)
4347
ListRepository(repo string) ([]string, error)
48+
ListRepositoryWithPagination(repo string, filter ListFilter) (tagmodels.ListResponse, error)
4449
Replicate(tag string) error
4550
Origin() (string, error)
4651

@@ -54,6 +59,12 @@ type singleClient struct {
5459
tls *tls.Config
5560
}
5661

62+
// ListFilter contains filter request for list with pagination operations.
63+
type ListFilter struct {
64+
Offset string
65+
Limit int
66+
}
67+
5768
// NewSingleClient returns a Client scoped to a single tagserver instance.
5869
func NewSingleClient(addr string, config *tls.Config) Client {
5970
return &singleClient{addr, config}
@@ -112,37 +123,90 @@ func (c *singleClient) Has(tag string) (bool, error) {
112123
return true, nil
113124
}
114125

115-
func (c *singleClient) List(prefix string) ([]string, error) {
116-
resp, err := httputil.Get(
117-
fmt.Sprintf("http://%s/list/%s", c.addr, prefix),
126+
func (c *singleClient) doListPaginated(urlFormat string, pathSub string,
127+
filter ListFilter) (tagmodels.ListResponse, error) {
128+
129+
// Build query.
130+
reqVal := url.Values{}
131+
if filter.Offset != "" {
132+
reqVal.Add(tagmodels.OffsetQ, filter.Offset)
133+
}
134+
if filter.Limit != 0 {
135+
reqVal.Add(tagmodels.LimitQ, strconv.Itoa(filter.Limit))
136+
}
137+
138+
// Fetch list response from server.
139+
serverUrl := url.URL{
140+
Scheme: "http",
141+
Host: c.addr,
142+
Path: fmt.Sprintf(urlFormat, pathSub),
143+
RawQuery: reqVal.Encode(),
144+
}
145+
var resp tagmodels.ListResponse
146+
httpResp, err := httputil.Get(
147+
serverUrl.String(),
118148
httputil.SendTimeout(60*time.Second),
119149
httputil.SendTLS(c.tls))
120150
if err != nil {
121-
return nil, err
151+
return resp, err
122152
}
123-
defer resp.Body.Close()
153+
defer httpResp.Body.Close()
154+
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
155+
return resp, fmt.Errorf("json decode: %s", err)
156+
}
157+
158+
return resp, nil
159+
}
160+
161+
func (c *singleClient) doList(pathSub string,
162+
fn func(pathSub string, filter ListFilter) (tagmodels.ListResponse, error)) (
163+
[]string, error) {
164+
124165
var names []string
125-
if err := json.NewDecoder(resp.Body).Decode(&names); err != nil {
126-
return nil, fmt.Errorf("json decode: %s", err)
166+
167+
offset := ""
168+
for ok := true; ok; ok = (offset != "") {
169+
filter := ListFilter{Offset: offset}
170+
resp, err := fn(pathSub, filter)
171+
if err != nil {
172+
return nil, err
173+
}
174+
offset, err = resp.GetOffset()
175+
if err != nil && err != io.EOF {
176+
return nil, err
177+
}
178+
names = append(names, resp.Result...)
127179
}
128180
return names, nil
129181
}
130182

183+
func (c *singleClient) List(prefix string) ([]string, error) {
184+
return c.doList(prefix, func(prefix string, filter ListFilter) (
185+
tagmodels.ListResponse, error) {
186+
187+
return c.ListWithPagination(prefix, filter)
188+
})
189+
}
190+
191+
func (c *singleClient) ListWithPagination(prefix string, filter ListFilter) (
192+
tagmodels.ListResponse, error) {
193+
194+
return c.doListPaginated("list/%s", prefix, filter)
195+
}
196+
131197
// XXX: Deprecated. Use List instead.
132198
func (c *singleClient) ListRepository(repo string) ([]string, error) {
133-
resp, err := httputil.Get(
134-
fmt.Sprintf("http://%s/repositories/%s/tags", c.addr, url.PathEscape(repo)),
135-
httputil.SendTimeout(60*time.Second),
136-
httputil.SendTLS(c.tls))
137-
if err != nil {
138-
return nil, err
139-
}
140-
defer resp.Body.Close()
141-
var tags []string
142-
if err := json.NewDecoder(resp.Body).Decode(&tags); err != nil {
143-
return nil, fmt.Errorf("json decode: %s", err)
144-
}
145-
return tags, nil
199+
return c.doList(repo, func(repo string, filter ListFilter) (
200+
tagmodels.ListResponse, error) {
201+
202+
return c.ListRepositoryWithPagination(repo, filter)
203+
})
204+
}
205+
206+
func (c *singleClient) ListRepositoryWithPagination(repo string,
207+
filter ListFilter) (tagmodels.ListResponse, error) {
208+
209+
return c.doListPaginated("repositories/%s/tags", url.PathEscape(repo), filter)
146210
}
147211

148212
// ReplicateRequest defines a Replicate request body.
@@ -279,6 +343,16 @@ func (cc *clusterClient) List(prefix string) (tags []string, err error) {
279343
return
280344
}
281345

346+
func (cc *clusterClient) ListWithPagination(prefix string, filter ListFilter) (
347+
resp tagmodels.ListResponse, err error) {
348+
349+
err = cc.do(func(c Client) error {
350+
resp, err = c.ListWithPagination(prefix, filter)
351+
return err
352+
})
353+
return
354+
}
355+
282356
func (cc *clusterClient) ListRepository(repo string) (tags []string, err error) {
283357
err = cc.do(func(c Client) error {
284358
tags, err = c.ListRepository(repo)
@@ -287,6 +361,16 @@ func (cc *clusterClient) ListRepository(repo string) (tags []string, err error)
287361
return
288362
}
289363

364+
func (cc *clusterClient) ListRepositoryWithPagination(repo string,
365+
filter ListFilter) (resp tagmodels.ListResponse, err error) {
366+
367+
err = cc.do(func(c Client) error {
368+
resp, err = c.ListRepositoryWithPagination(repo, filter)
369+
return err
370+
})
371+
return
372+
}
373+
290374
func (cc *clusterClient) Replicate(tag string) error {
291375
return cc.do(func(c Client) error { return c.Replicate(tag) })
292376
}

build-index/tagmodels/models.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2016-2019 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package tagmodels
15+
16+
import (
17+
"fmt"
18+
"io"
19+
"net/url"
20+
)
21+
22+
const (
23+
// Filters.
24+
LimitQ string = "limit"
25+
OffsetQ string = "offset"
26+
)
27+
28+
// List Response with pagination. Models tagserver reponse to list and
29+
// listRepository.
30+
type ListResponse struct {
31+
Links struct {
32+
Next string `json:"next"`
33+
Self string `json:"self"`
34+
}
35+
Size int `json:"size"`
36+
Result []string `json:"result"`
37+
}
38+
39+
// GetOffset returns offset token from the ListResponse struct.
40+
// Returns token if present, io.EOF if Next is empty, error otherwise.
41+
func (resp ListResponse) GetOffset() (string, error) {
42+
if resp.Links.Next == "" {
43+
return "", io.EOF
44+
}
45+
46+
nextUrl, err := url.Parse(resp.Links.Next)
47+
if err != nil {
48+
return "", err
49+
}
50+
val, err := url.ParseQuery(nextUrl.RawQuery)
51+
if err != nil {
52+
return "", err
53+
}
54+
offset := val.Get(OffsetQ)
55+
if offset == "" {
56+
return "", fmt.Errorf("invalid offset in %s", resp.Links.Next)
57+
}
58+
return offset, nil
59+
}

build-index/tagserver/server.go

Lines changed: 26 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"time"
2626

2727
"github.com/uber/kraken/build-index/tagclient"
28+
"github.com/uber/kraken/build-index/tagmodels"
2829
"github.com/uber/kraken/build-index/tagstore"
2930
"github.com/uber/kraken/build-index/tagtype"
3031
"github.com/uber/kraken/core"
@@ -45,11 +46,6 @@ import (
4546
"github.com/uber-go/tally"
4647
)
4748

48-
const (
49-
limitQ string = "limit"
50-
offsetQ string = "offset"
51-
)
52-
5349
// Server provides tag operations for the build-index.
5450
type Server struct {
5551
config Config
@@ -69,16 +65,6 @@ type Server struct {
6965
depResolver tagtype.DependencyResolver
7066
}
7167

72-
// List Response with pagination.
73-
type ListResponse struct {
74-
Links struct {
75-
Next string `json:"next"`
76-
Self string `json:"self"`
77-
}
78-
Size int `json:"size"`
79-
Result []string `json:"result"`
80-
}
81-
8268
// New creates a new Server.
8369
func New(
8470
config Config,
@@ -253,6 +239,8 @@ func (s *Server) hasTagHandler(w http.ResponseWriter, r *http.Request) error {
253239
return nil
254240
}
255241

242+
// listHandler handles list images request. Response model
243+
// tagmodels.ListResponse.
256244
func (s *Server) listHandler(w http.ResponseWriter, r *http.Request) error {
257245
prefix := r.URL.Path[len("/list/"):]
258246

@@ -282,6 +270,8 @@ func (s *Server) listHandler(w http.ResponseWriter, r *http.Request) error {
282270
return nil
283271
}
284272

273+
// listRepositoryHandler handles list images tag request. Response model
274+
// tagmodels.ListResponse.
285275
// TODO(codyg): Remove this.
286276
func (s *Server) listRepositoryHandler(w http.ResponseWriter, r *http.Request) error {
287277
repo, err := httputil.ParseParam(r, "repo")
@@ -455,7 +445,7 @@ func buildPaginationOptions(u *url.URL) ([]backend.ListOption, error) {
455445
"invalid query %s:%s", k, v).Status(http.StatusBadRequest)
456446
}
457447
switch k {
458-
case limitQ:
448+
case tagmodels.LimitQ:
459449
limitCount, err := strconv.Atoi(v[0])
460450
if err != nil {
461451
return nil, handler.Errorf(
@@ -466,7 +456,7 @@ func buildPaginationOptions(u *url.URL) ([]backend.ListOption, error) {
466456
"invalid limit %d", limitCount).Status(http.StatusBadRequest)
467457
}
468458
opts = append(opts, backend.ListWithMaxKeys(limitCount))
469-
case offsetQ:
459+
case tagmodels.OffsetQ:
470460
opts = append(opts, backend.ListWithContinuationToken(v[0]))
471461
default:
472462
return nil, handler.Errorf(
@@ -482,31 +472,31 @@ func buildPaginationOptions(u *url.URL) ([]backend.ListOption, error) {
482472
}
483473

484474
func buildPaginationResponse(u *url.URL, continuationToken string,
485-
result []string) (interface{}, error) {
486-
487-
if continuationToken == "" {
488-
return result, nil
489-
}
475+
result []string) (*tagmodels.ListResponse, error) {
490476

491-
// Deep copy url.
492-
nextUrl, err := url.Parse(u.String())
493-
if err != nil {
494-
return nil, handler.Errorf(
495-
"invalid url string: %s", err).Status(http.StatusBadRequest)
496-
}
497-
v := url.Values{}
498-
if limit := u.Query().Get(limitQ); limit != "" {
499-
v.Add(limitQ, limit)
477+
nextUrlString := ""
478+
if continuationToken != "" {
479+
// Deep copy url.
480+
nextUrl, err := url.Parse(u.String())
481+
if err != nil {
482+
return nil, handler.Errorf(
483+
"invalid url string: %s", err).Status(http.StatusBadRequest)
484+
}
485+
v := url.Values{}
486+
if limit := u.Query().Get(tagmodels.LimitQ); limit != "" {
487+
v.Add(tagmodels.LimitQ, limit)
488+
}
489+
// ContinuationToken cannot be empty here.
490+
v.Add(tagmodels.OffsetQ, continuationToken)
491+
nextUrl.RawQuery = v.Encode()
492+
nextUrlString = nextUrl.String()
500493
}
501-
// ContinuationToken cannot be empty here.
502-
v.Add(offsetQ, continuationToken)
503-
nextUrl.RawQuery = v.Encode()
504494

505-
resp := ListResponse{
495+
resp := tagmodels.ListResponse{
506496
Size: len(result),
507497
Result: result,
508498
}
509-
resp.Links.Next = nextUrl.String()
499+
resp.Links.Next = nextUrlString
510500
resp.Links.Self = u.String()
511501

512502
return &resp, nil

0 commit comments

Comments
 (0)