Skip to content

Commit

Permalink
Merge pull request distribution#653 from pdevine/catalog-api
Browse files Browse the repository at this point in the history
Catalog for V2 API Implementation
  • Loading branch information
stevvooe committed Jul 23, 2015
2 parents c90d31c + e44b6fd commit 8d2d714
Show file tree
Hide file tree
Showing 10 changed files with 835 additions and 1 deletion.
127 changes: 127 additions & 0 deletions registry/api/v2/descriptors.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,30 @@ var (
Format: "<digest>",
}

linkHeader = ParameterDescriptor{
Name: "Link",
Type: "link",
Description: "RFC5988 compliant rel='next' with URL to next result set, if available",
Format: `<<url>?n=<last n value>&last=<last entry from response>>; rel="next"`,
}

paginationParameters = []ParameterDescriptor{
{
Name: "n",
Type: "integer",
Description: "Limit the number of entries in each response. It not present, all entries will be returned.",
Format: "<integer>",
Required: false,
},
{
Name: "last",
Type: "string",
Description: "Result set will include values lexically after last.",
Format: "<integer>",
Required: false,
},
}

unauthorizedResponse = ResponseDescriptor{
Description: "The client does not have access to the repository.",
StatusCode: http.StatusUnauthorized,
Expand Down Expand Up @@ -269,6 +293,9 @@ type ResponseDescriptor struct {
// Headers covers any headers that may be returned from the response.
Headers []ParameterDescriptor

// Fields describes any fields that may be present in the response.
Fields []ParameterDescriptor

// ErrorCodes enumerates the error codes that may be returned along with
// the response.
ErrorCodes []errcode.ErrorCode
Expand Down Expand Up @@ -427,6 +454,36 @@ var routeDescriptors = []RouteDescriptor{
},
},
},
{
Description: "Return a portion of the tags for the specified repository.",
PathParameters: []ParameterDescriptor{nameParameterDescriptor},
QueryParameters: paginationParameters,
Successes: []ResponseDescriptor{
{
StatusCode: http.StatusOK,
Description: "A list of tags for the named repository.",
Headers: []ParameterDescriptor{
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON response body.",
Format: "<length>",
},
linkHeader,
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: `{
"name": <name>,
"tags": [
<tag>,
...
],
}`,
},
},
},
},
},
},
},
Expand Down Expand Up @@ -1320,6 +1377,76 @@ var routeDescriptors = []RouteDescriptor{
},
},
},
{
Name: RouteNameCatalog,
Path: "/v2/_catalog",
Entity: "Catalog",
Description: "List a set of available repositories in the local registry cluster. Does not provide any indication of what may be available upstream. Applications can only determine if a repository is available but not if it is not available.",
Methods: []MethodDescriptor{
{
Method: "GET",
Description: "Retrieve a sorted, json list of repositories available in the registry.",
Requests: []RequestDescriptor{
{
Name: "Catalog Fetch Complete",
Description: "Request an unabridged list of repositories available.",
Successes: []ResponseDescriptor{
{
Description: "Returns the unabridged list of repositories as a json response.",
StatusCode: http.StatusOK,
Headers: []ParameterDescriptor{
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON response body.",
Format: "<length>",
},
},
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: `{
"repositories": [
<name>,
...
]
}`,
},
},
},
},
{
Name: "Catalog Fetch Paginated",
Description: "Return the specified portion of repositories.",
QueryParameters: paginationParameters,
Successes: []ResponseDescriptor{
{
StatusCode: http.StatusOK,
Body: BodyDescriptor{
ContentType: "application/json; charset=utf-8",
Format: `{
"repositories": [
<name>,
...
]
"next": "<url>?last=<name>&n=<last value of n>"
}`,
},
Headers: []ParameterDescriptor{
{
Name: "Content-Length",
Type: "integer",
Description: "Length of the JSON response body.",
Format: "<length>",
},
linkHeader,
},
},
},
},
},
},
},
},
}

var routeDescriptorsMap map[string]RouteDescriptor
Expand Down
2 changes: 2 additions & 0 deletions registry/api/v2/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ const (
RouteNameBlob = "blob"
RouteNameBlobUpload = "blob-upload"
RouteNameBlobUploadChunk = "blob-upload-chunk"
RouteNameCatalog = "catalog"
)

var allEndpoints = []string{
RouteNameManifest,
RouteNameCatalog,
RouteNameTags,
RouteNameBlob,
RouteNameBlobUpload,
Expand Down
12 changes: 12 additions & 0 deletions registry/api/v2/urls.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ func (ub *URLBuilder) BuildBaseURL() (string, error) {
return baseURL.String(), nil
}

// BuildCatalogURL constructs a url get a catalog of repositories
func (ub *URLBuilder) BuildCatalogURL(values ...url.Values) (string, error) {
route := ub.cloneRoute(RouteNameCatalog)

catalogURL, err := route.URL()
if err != nil {
return "", err
}

return appendValuesURL(catalogURL, values...).String(), nil
}

// BuildTagsURL constructs a url to list the tags in the named repository.
func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
route := ub.cloneRoute(RouteNameTags)
Expand Down
91 changes: 91 additions & 0 deletions registry/client/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,83 @@ import (
"github.com/docker/distribution/registry/storage/cache/memory"
)

// Registry provides an interface for calling Repositories, which returns a catalog of repositories.
type Registry interface {
Repositories(ctx context.Context, repos []string, last string) (n int, err error)
}

// NewRegistry creates a registry namespace which can be used to get a listing of repositories
func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) {
ub, err := v2.NewURLBuilderFromString(baseURL)
if err != nil {
return nil, err
}

client := &http.Client{
Transport: transport,
Timeout: 1 * time.Minute,
}

return &registry{
client: client,
ub: ub,
context: ctx,
}, nil
}

type registry struct {
client *http.Client
ub *v2.URLBuilder
context context.Context
}

// Repositories returns a lexigraphically sorted catalog given a base URL. The 'entries' slice will be filled up to the size
// of the slice, starting at the value provided in 'last'. The number of entries will be returned along with io.EOF if there
// are no more entries
func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) {
var numFilled int
var returnErr error

values := buildCatalogValues(len(entries), last)
u, err := r.ub.BuildCatalogURL(values)
if err != nil {
return 0, err
}

resp, err := r.client.Get(u)
if err != nil {
return 0, err
}
defer resp.Body.Close()

switch resp.StatusCode {
case http.StatusOK:
var ctlg struct {
Repositories []string `json:"repositories"`
}
decoder := json.NewDecoder(resp.Body)

if err := decoder.Decode(&ctlg); err != nil {
return 0, err
}

for cnt := range ctlg.Repositories {
entries[cnt] = ctlg.Repositories[cnt]
}
numFilled = len(ctlg.Repositories)

link := resp.Header.Get("Link")
if link == "" {
returnErr = io.EOF
}

default:
return 0, handleErrorResponse(resp)
}

return numFilled, returnErr
}

// NewRepository creates a new Repository for the given repository name and base URL
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
if err := v2.ValidateRepositoryName(name); err != nil {
Expand Down Expand Up @@ -444,3 +521,17 @@ func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distributi
return distribution.Descriptor{}, handleErrorResponse(resp)
}
}

func buildCatalogValues(maxEntries int, last string) url.Values {
values := url.Values{}

if maxEntries > 0 {
values.Add("n", strconv.Itoa(maxEntries))
}

if last != "" {
values.Add("last", last)
}

return values
}
Loading

0 comments on commit 8d2d714

Please sign in to comment.