commit
						a1ce8d81f7
					
				| 
						 | 
					@ -87,6 +87,30 @@ var (
 | 
				
			||||||
		Format:      "<digest>",
 | 
							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{
 | 
						unauthorizedResponse = ResponseDescriptor{
 | 
				
			||||||
		Description: "The client does not have access to the repository.",
 | 
							Description: "The client does not have access to the repository.",
 | 
				
			||||||
		StatusCode:  http.StatusUnauthorized,
 | 
							StatusCode:  http.StatusUnauthorized,
 | 
				
			||||||
| 
						 | 
					@ -269,6 +293,9 @@ type ResponseDescriptor struct {
 | 
				
			||||||
	// Headers covers any headers that may be returned from the response.
 | 
						// Headers covers any headers that may be returned from the response.
 | 
				
			||||||
	Headers []ParameterDescriptor
 | 
						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
 | 
						// ErrorCodes enumerates the error codes that may be returned along with
 | 
				
			||||||
	// the response.
 | 
						// the response.
 | 
				
			||||||
	ErrorCodes []errcode.ErrorCode
 | 
						ErrorCodes []errcode.ErrorCode
 | 
				
			||||||
| 
						 | 
					@ -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>,
 | 
				
			||||||
 | 
					        ...
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					}`,
 | 
				
			||||||
 | 
													},
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
				},
 | 
									},
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					var routeDescriptorsMap map[string]RouteDescriptor
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,10 +11,12 @@ const (
 | 
				
			||||||
	RouteNameBlob            = "blob"
 | 
						RouteNameBlob            = "blob"
 | 
				
			||||||
	RouteNameBlobUpload      = "blob-upload"
 | 
						RouteNameBlobUpload      = "blob-upload"
 | 
				
			||||||
	RouteNameBlobUploadChunk = "blob-upload-chunk"
 | 
						RouteNameBlobUploadChunk = "blob-upload-chunk"
 | 
				
			||||||
 | 
						RouteNameCatalog         = "catalog"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var allEndpoints = []string{
 | 
					var allEndpoints = []string{
 | 
				
			||||||
	RouteNameManifest,
 | 
						RouteNameManifest,
 | 
				
			||||||
 | 
						RouteNameCatalog,
 | 
				
			||||||
	RouteNameTags,
 | 
						RouteNameTags,
 | 
				
			||||||
	RouteNameBlob,
 | 
						RouteNameBlob,
 | 
				
			||||||
	RouteNameBlobUpload,
 | 
						RouteNameBlobUpload,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,6 +100,18 @@ func (ub *URLBuilder) BuildBaseURL() (string, error) {
 | 
				
			||||||
	return baseURL.String(), nil
 | 
						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.
 | 
					// BuildTagsURL constructs a url to list the tags in the named repository.
 | 
				
			||||||
func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
 | 
					func (ub *URLBuilder) BuildTagsURL(name string) (string, error) {
 | 
				
			||||||
	route := ub.cloneRoute(RouteNameTags)
 | 
						route := ub.cloneRoute(RouteNameTags)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,6 +21,83 @@ import (
 | 
				
			||||||
	"github.com/docker/distribution/registry/storage/cache/memory"
 | 
						"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 ®istry{
 | 
				
			||||||
 | 
							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
 | 
					// 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) {
 | 
					func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
 | 
				
			||||||
	if err := v2.ValidateRepositoryName(name); err != nil {
 | 
						if err := v2.ValidateRepositoryName(name); err != nil {
 | 
				
			||||||
| 
						 | 
					@ -444,3 +521,17 @@ func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distributi
 | 
				
			||||||
		return distribution.Descriptor{}, handleErrorResponse(resp)
 | 
							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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,9 +5,11 @@ import (
 | 
				
			||||||
	"crypto/rand"
 | 
						"crypto/rand"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/http/httptest"
 | 
						"net/http/httptest"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
| 
						 | 
					@ -77,6 +79,28 @@ func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.R
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) {
 | 
				
			||||||
 | 
						headers := map[string][]string{
 | 
				
			||||||
 | 
							"Content-Length": {strconv.Itoa(len(content))},
 | 
				
			||||||
 | 
							"Content-Type":   {"application/json; charset=utf-8"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if link != "" {
 | 
				
			||||||
 | 
							headers["Link"] = append(headers["Link"], link)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*m = append(*m, testutil.RequestResponseMapping{
 | 
				
			||||||
 | 
							Request: testutil.Request{
 | 
				
			||||||
 | 
								Method: "GET",
 | 
				
			||||||
 | 
								Route:  route,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Response: testutil.Response{
 | 
				
			||||||
 | 
								StatusCode: http.StatusOK,
 | 
				
			||||||
 | 
								Body:       content,
 | 
				
			||||||
 | 
								Headers:    http.Header(headers),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestBlobFetch(t *testing.T) {
 | 
					func TestBlobFetch(t *testing.T) {
 | 
				
			||||||
	d1, b1 := newRandomBlob(1024)
 | 
						d1, b1 := newRandomBlob(1024)
 | 
				
			||||||
	var m testutil.RequestResponseMap
 | 
						var m testutil.RequestResponseMap
 | 
				
			||||||
| 
						 | 
					@ -732,3 +756,71 @@ func TestManifestUnauthorized(t *testing.T) {
 | 
				
			||||||
		t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
 | 
							t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCatalog(t *testing.T) {
 | 
				
			||||||
 | 
						var m testutil.RequestResponseMap
 | 
				
			||||||
 | 
						addTestCatalog(
 | 
				
			||||||
 | 
							"/v2/_catalog?n=5",
 | 
				
			||||||
 | 
							[]byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						e, c := testServer(m)
 | 
				
			||||||
 | 
						defer c()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						entries := make([]string, 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						r, err := NewRegistry(context.Background(), e, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						numFilled, err := r.Repositories(ctx, entries, "")
 | 
				
			||||||
 | 
						if err != io.EOF {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if numFilled != 3 {
 | 
				
			||||||
 | 
							t.Fatalf("Got wrong number of repos")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCatalogInParts(t *testing.T) {
 | 
				
			||||||
 | 
						var m testutil.RequestResponseMap
 | 
				
			||||||
 | 
						addTestCatalog(
 | 
				
			||||||
 | 
							"/v2/_catalog?n=2",
 | 
				
			||||||
 | 
							[]byte("{\"repositories\":[\"bar\", \"baz\"]}"),
 | 
				
			||||||
 | 
							"</v2/_catalog?last=baz&n=2>", &m)
 | 
				
			||||||
 | 
						addTestCatalog(
 | 
				
			||||||
 | 
							"/v2/_catalog?last=baz&n=2",
 | 
				
			||||||
 | 
							[]byte("{\"repositories\":[\"foo\"]}"),
 | 
				
			||||||
 | 
							"", &m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						e, c := testServer(m)
 | 
				
			||||||
 | 
						defer c()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						entries := make([]string, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						r, err := NewRegistry(context.Background(), e, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						numFilled, err := r.Repositories(ctx, entries, "")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if numFilled != 2 {
 | 
				
			||||||
 | 
							t.Fatalf("Got wrong number of repos")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						numFilled, err = r.Repositories(ctx, entries, "baz")
 | 
				
			||||||
 | 
						if err != io.EOF {
 | 
				
			||||||
 | 
							t.Fatal(err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if numFilled != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("Got wrong number of repos")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,8 @@ import (
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path"
 | 
						"path"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -60,6 +62,152 @@ func TestCheckAPI(t *testing.T) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TestCatalogAPI tests the /v2/_catalog endpoint
 | 
				
			||||||
 | 
					func TestCatalogAPI(t *testing.T) {
 | 
				
			||||||
 | 
						chunkLen := 2
 | 
				
			||||||
 | 
						env := newTestEnv(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						values := url.Values{
 | 
				
			||||||
 | 
							"last": []string{""},
 | 
				
			||||||
 | 
							"n":    []string{strconv.Itoa(chunkLen)}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						catalogURL, err := env.builder.BuildCatalogURL(values)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("unexpected error building catalog url: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// -----------------------------------
 | 
				
			||||||
 | 
						// try to get an empty catalog
 | 
				
			||||||
 | 
						resp, err := http.Get(catalogURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("unexpected error issuing request: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var ctlg struct {
 | 
				
			||||||
 | 
							Repositories []string `json:"repositories"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dec := json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
						if err := dec.Decode(&ctlg); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("error decoding fetched manifest: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// we haven't pushed anything to the registry yet
 | 
				
			||||||
 | 
						if len(ctlg.Repositories) != 0 {
 | 
				
			||||||
 | 
							t.Fatalf("repositories has unexpected values")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if resp.Header.Get("Link") != "" {
 | 
				
			||||||
 | 
							t.Fatalf("repositories has more data when none expected")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// -----------------------------------
 | 
				
			||||||
 | 
						// push something to the registry and try again
 | 
				
			||||||
 | 
						images := []string{"foo/aaaa", "foo/bbbb", "foo/cccc"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, image := range images {
 | 
				
			||||||
 | 
							createRepository(env, t, image, "sometag")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err = http.Get(catalogURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("unexpected error issuing request: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dec = json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
						if err = dec.Decode(&ctlg); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("error decoding fetched manifest: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(ctlg.Repositories) != chunkLen {
 | 
				
			||||||
 | 
							t.Fatalf("repositories has unexpected values")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, image := range images[:chunkLen] {
 | 
				
			||||||
 | 
							if !contains(ctlg.Repositories, image) {
 | 
				
			||||||
 | 
								t.Fatalf("didn't find our repository '%s' in the catalog", image)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						link := resp.Header.Get("Link")
 | 
				
			||||||
 | 
						if link == "" {
 | 
				
			||||||
 | 
							t.Fatalf("repositories has less data than expected")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newValues := checkLink(t, link, chunkLen, ctlg.Repositories[len(ctlg.Repositories)-1])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// -----------------------------------
 | 
				
			||||||
 | 
						// get the last chunk of data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						catalogURL, err = env.builder.BuildCatalogURL(newValues)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("unexpected error building catalog url: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp, err = http.Get(catalogURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("unexpected error issuing request: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						checkResponse(t, "issuing catalog api check", resp, http.StatusOK)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dec = json.NewDecoder(resp.Body)
 | 
				
			||||||
 | 
						if err = dec.Decode(&ctlg); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("error decoding fetched manifest: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(ctlg.Repositories) != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("repositories has unexpected values")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lastImage := images[len(images)-1]
 | 
				
			||||||
 | 
						if !contains(ctlg.Repositories, lastImage) {
 | 
				
			||||||
 | 
							t.Fatalf("didn't find our repository '%s' in the catalog", lastImage)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						link = resp.Header.Get("Link")
 | 
				
			||||||
 | 
						if link != "" {
 | 
				
			||||||
 | 
							t.Fatalf("catalog has unexpected data")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func checkLink(t *testing.T, urlStr string, numEntries int, last string) url.Values {
 | 
				
			||||||
 | 
						re := regexp.MustCompile("<(/v2/_catalog.*)>; rel=\"next\"")
 | 
				
			||||||
 | 
						matches := re.FindStringSubmatch(urlStr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(matches) != 2 {
 | 
				
			||||||
 | 
							t.Fatalf("Catalog link address response was incorrect")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						linkURL, _ := url.Parse(matches[1])
 | 
				
			||||||
 | 
						urlValues := linkURL.Query()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if urlValues.Get("n") != strconv.Itoa(numEntries) {
 | 
				
			||||||
 | 
							t.Fatalf("Catalog link entry size is incorrect")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if urlValues.Get("last") != last {
 | 
				
			||||||
 | 
							t.Fatal("Catalog link last entry is incorrect")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return urlValues
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func contains(elems []string, e string) bool {
 | 
				
			||||||
 | 
						for _, elem := range elems {
 | 
				
			||||||
 | 
							if elem == e {
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestURLPrefix(t *testing.T) {
 | 
					func TestURLPrefix(t *testing.T) {
 | 
				
			||||||
	config := configuration.Configuration{
 | 
						config := configuration.Configuration{
 | 
				
			||||||
		Storage: configuration.Storage{
 | 
							Storage: configuration.Storage{
 | 
				
			||||||
| 
						 | 
					@ -869,3 +1017,60 @@ func checkErr(t *testing.T, err error, msg string) {
 | 
				
			||||||
		t.Fatalf("unexpected error %s: %v", msg, err)
 | 
							t.Fatalf("unexpected error %s: %v", msg, err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func createRepository(env *testEnv, t *testing.T, imageName string, tag string) {
 | 
				
			||||||
 | 
						unsignedManifest := &manifest.Manifest{
 | 
				
			||||||
 | 
							Versioned: manifest.Versioned{
 | 
				
			||||||
 | 
								SchemaVersion: 1,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							Name: imageName,
 | 
				
			||||||
 | 
							Tag:  tag,
 | 
				
			||||||
 | 
							FSLayers: []manifest.FSLayer{
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									BlobSum: "asdf",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									BlobSum: "qwer",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Push 2 random layers
 | 
				
			||||||
 | 
						expectedLayers := make(map[digest.Digest]io.ReadSeeker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for i := range unsignedManifest.FSLayers {
 | 
				
			||||||
 | 
							rs, dgstStr, err := testutil.CreateRandomTarFile()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("error creating random layer %d: %v", i, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							dgst := digest.Digest(dgstStr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							expectedLayers[dgst] = rs
 | 
				
			||||||
 | 
							unsignedManifest.FSLayers[i].BlobSum = dgst
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							uploadURLBase, _ := startPushLayer(t, env.builder, imageName)
 | 
				
			||||||
 | 
							pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						signedManifest, err := manifest.Sign(unsignedManifest, env.pk)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("unexpected error signing manifest: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						payload, err := signedManifest.Payload()
 | 
				
			||||||
 | 
						checkErr(t, err, "getting manifest payload")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dgst, err := digest.FromBytes(payload)
 | 
				
			||||||
 | 
						checkErr(t, err, "digesting manifest")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						manifestDigestURL, err := env.builder.BuildManifestURL(imageName, dgst.String())
 | 
				
			||||||
 | 
						checkErr(t, err, "building manifest url")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						resp := putManifest(t, "putting signed manifest", manifestDigestURL, signedManifest)
 | 
				
			||||||
 | 
						checkResponse(t, "putting signed manifest", resp, http.StatusAccepted)
 | 
				
			||||||
 | 
						checkHeaders(t, resp, http.Header{
 | 
				
			||||||
 | 
							"Location":              []string{manifestDigestURL},
 | 
				
			||||||
 | 
							"Docker-Content-Digest": []string{dgst.String()},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,6 +69,7 @@ func NewApp(ctx context.Context, configuration configuration.Configuration) *App
 | 
				
			||||||
		return http.HandlerFunc(apiBase)
 | 
							return http.HandlerFunc(apiBase)
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	app.register(v2.RouteNameManifest, imageManifestDispatcher)
 | 
						app.register(v2.RouteNameManifest, imageManifestDispatcher)
 | 
				
			||||||
 | 
						app.register(v2.RouteNameCatalog, catalogDispatcher)
 | 
				
			||||||
	app.register(v2.RouteNameTags, tagsDispatcher)
 | 
						app.register(v2.RouteNameTags, tagsDispatcher)
 | 
				
			||||||
	app.register(v2.RouteNameBlob, blobDispatcher)
 | 
						app.register(v2.RouteNameBlob, blobDispatcher)
 | 
				
			||||||
	app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
 | 
						app.register(v2.RouteNameBlobUpload, blobUploadDispatcher)
 | 
				
			||||||
| 
						 | 
					@ -493,6 +494,7 @@ func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Cont
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return fmt.Errorf("forbidden: no repository name")
 | 
								return fmt.Errorf("forbidden: no repository name")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							accessRecords = appendCatalogAccessRecord(accessRecords, r)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ctx, err := app.accessController.Authorized(context.Context, accessRecords...)
 | 
						ctx, err := app.accessController.Authorized(context.Context, accessRecords...)
 | 
				
			||||||
| 
						 | 
					@ -538,7 +540,8 @@ func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listene
 | 
				
			||||||
// nameRequired returns true if the route requires a name.
 | 
					// nameRequired returns true if the route requires a name.
 | 
				
			||||||
func (app *App) nameRequired(r *http.Request) bool {
 | 
					func (app *App) nameRequired(r *http.Request) bool {
 | 
				
			||||||
	route := mux.CurrentRoute(r)
 | 
						route := mux.CurrentRoute(r)
 | 
				
			||||||
	return route == nil || route.GetName() != v2.RouteNameBase
 | 
						routeName := route.GetName()
 | 
				
			||||||
 | 
						return route == nil || (routeName != v2.RouteNameBase && routeName != v2.RouteNameCatalog)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// apiBase implements a simple yes-man for doing overall checks against the
 | 
					// apiBase implements a simple yes-man for doing overall checks against the
 | 
				
			||||||
| 
						 | 
					@ -588,6 +591,26 @@ func appendAccessRecords(records []auth.Access, method string, repo string) []au
 | 
				
			||||||
	return records
 | 
						return records
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Add the access record for the catalog if it's our current route
 | 
				
			||||||
 | 
					func appendCatalogAccessRecord(accessRecords []auth.Access, r *http.Request) []auth.Access {
 | 
				
			||||||
 | 
						route := mux.CurrentRoute(r)
 | 
				
			||||||
 | 
						routeName := route.GetName()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if routeName == v2.RouteNameCatalog {
 | 
				
			||||||
 | 
							resource := auth.Resource{
 | 
				
			||||||
 | 
								Type: "registry",
 | 
				
			||||||
 | 
								Name: "catalog",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							accessRecords = append(accessRecords,
 | 
				
			||||||
 | 
								auth.Access{
 | 
				
			||||||
 | 
									Resource: resource,
 | 
				
			||||||
 | 
									Action:   "*",
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return accessRecords
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// applyRegistryMiddleware wraps a registry instance with the configured middlewares
 | 
					// applyRegistryMiddleware wraps a registry instance with the configured middlewares
 | 
				
			||||||
func applyRegistryMiddleware(registry distribution.Namespace, middlewares []configuration.Middleware) (distribution.Namespace, error) {
 | 
					func applyRegistryMiddleware(registry distribution.Namespace, middlewares []configuration.Middleware) (distribution.Namespace, error) {
 | 
				
			||||||
	for _, mw := range middlewares {
 | 
						for _, mw := range middlewares {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,95 @@
 | 
				
			||||||
 | 
					package handlers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/distribution/registry/api/errcode"
 | 
				
			||||||
 | 
						"github.com/gorilla/handlers"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const maximumReturnedEntries = 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func catalogDispatcher(ctx *Context, r *http.Request) http.Handler {
 | 
				
			||||||
 | 
						catalogHandler := &catalogHandler{
 | 
				
			||||||
 | 
							Context: ctx,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return handlers.MethodHandler{
 | 
				
			||||||
 | 
							"GET": http.HandlerFunc(catalogHandler.GetCatalog),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type catalogHandler struct {
 | 
				
			||||||
 | 
						*Context
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type catalogAPIResponse struct {
 | 
				
			||||||
 | 
						Repositories []string `json:"repositories"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (ch *catalogHandler) GetCatalog(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
						var moreEntries = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						q := r.URL.Query()
 | 
				
			||||||
 | 
						lastEntry := q.Get("last")
 | 
				
			||||||
 | 
						maxEntries, err := strconv.Atoi(q.Get("n"))
 | 
				
			||||||
 | 
						if err != nil || maxEntries < 0 {
 | 
				
			||||||
 | 
							maxEntries = maximumReturnedEntries
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repos := make([]string, maxEntries)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filled, err := ch.App.registry.Repositories(ch.Context, repos, lastEntry)
 | 
				
			||||||
 | 
						if err == io.EOF {
 | 
				
			||||||
 | 
							moreEntries = false
 | 
				
			||||||
 | 
						} else if err != nil {
 | 
				
			||||||
 | 
							ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set("Content-Type", "application/json; charset=utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add a link header if there are more entries to retrieve
 | 
				
			||||||
 | 
						if moreEntries {
 | 
				
			||||||
 | 
							lastEntry = repos[len(repos)-1]
 | 
				
			||||||
 | 
							urlStr, err := createLinkEntry(r.URL.String(), maxEntries, lastEntry)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							w.Header().Set("Link", urlStr)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						enc := json.NewEncoder(w)
 | 
				
			||||||
 | 
						if err := enc.Encode(catalogAPIResponse{
 | 
				
			||||||
 | 
							Repositories: repos[0:filled],
 | 
				
			||||||
 | 
						}); err != nil {
 | 
				
			||||||
 | 
							ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err))
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Use the original URL from the request to create a new URL for
 | 
				
			||||||
 | 
					// the link header
 | 
				
			||||||
 | 
					func createLinkEntry(origURL string, maxEntries int, lastEntry string) (string, error) {
 | 
				
			||||||
 | 
						calledURL, err := url.Parse(origURL)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						v := url.Values{}
 | 
				
			||||||
 | 
						v.Add("n", strconv.Itoa(maxEntries))
 | 
				
			||||||
 | 
						v.Add("last", lastEntry)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						calledURL.RawQuery = v.Encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						calledURL.Fragment = ""
 | 
				
			||||||
 | 
						urlStr := fmt.Sprintf("<%s>; rel=\"next\"", calledURL.String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return urlStr, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					package storage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
 | 
						"sort"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/distribution/context"
 | 
				
			||||||
 | 
						"github.com/docker/distribution/registry/storage/driver"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Returns a list, or partial list, of repositories in the registry.
 | 
				
			||||||
 | 
					// Because it's a quite expensive operation, it should only be used when building up
 | 
				
			||||||
 | 
					// an initial set of repositories.
 | 
				
			||||||
 | 
					func (reg *registry) Repositories(ctx context.Context, repos []string, last string) (n int, err error) {
 | 
				
			||||||
 | 
						var foundRepos []string
 | 
				
			||||||
 | 
						var errVal error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(repos) == 0 {
 | 
				
			||||||
 | 
							return 0, errors.New("no space in slice")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						root, err := defaultPathMapper.path(repositoriesRootPathSpec{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Walk each of the directories in our storage.  Unfortunately since there's no
 | 
				
			||||||
 | 
						// guarantee that storage will return files in lexigraphical order, we have
 | 
				
			||||||
 | 
						// to store everything another slice, sort it and then copy it back to our
 | 
				
			||||||
 | 
						// passed in slice.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Walk(ctx, reg.blobStore.driver, root, func(fileInfo driver.FileInfo) error {
 | 
				
			||||||
 | 
							filePath := fileInfo.Path()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// lop the base path off
 | 
				
			||||||
 | 
							repoPath := filePath[len(root)+1:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							_, file := path.Split(repoPath)
 | 
				
			||||||
 | 
							if file == "_layers" {
 | 
				
			||||||
 | 
								repoPath = strings.TrimSuffix(repoPath, "/_layers")
 | 
				
			||||||
 | 
								if repoPath > last {
 | 
				
			||||||
 | 
									foundRepos = append(foundRepos, repoPath)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return ErrSkipDir
 | 
				
			||||||
 | 
							} else if strings.HasPrefix(file, "_") {
 | 
				
			||||||
 | 
								return ErrSkipDir
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sort.Strings(foundRepos)
 | 
				
			||||||
 | 
						n = copy(repos, foundRepos)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Signal that we have no more entries by setting EOF
 | 
				
			||||||
 | 
						if len(foundRepos) <= len(repos) {
 | 
				
			||||||
 | 
							errVal = io.EOF
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return n, errVal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,122 @@
 | 
				
			||||||
 | 
					package storage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/distribution"
 | 
				
			||||||
 | 
						"github.com/docker/distribution/context"
 | 
				
			||||||
 | 
						"github.com/docker/distribution/registry/storage/cache/memory"
 | 
				
			||||||
 | 
						"github.com/docker/distribution/registry/storage/driver"
 | 
				
			||||||
 | 
						"github.com/docker/distribution/registry/storage/driver/inmemory"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type setupEnv struct {
 | 
				
			||||||
 | 
						ctx      context.Context
 | 
				
			||||||
 | 
						driver   driver.StorageDriver
 | 
				
			||||||
 | 
						expected []string
 | 
				
			||||||
 | 
						registry distribution.Namespace
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func setupFS(t *testing.T) *setupEnv {
 | 
				
			||||||
 | 
						d := inmemory.New()
 | 
				
			||||||
 | 
						c := []byte("")
 | 
				
			||||||
 | 
						ctx := context.Background()
 | 
				
			||||||
 | 
						registry := NewRegistryWithDriver(ctx, d, memory.NewInMemoryBlobDescriptorCacheProvider())
 | 
				
			||||||
 | 
						rootpath, _ := defaultPathMapper.path(repositoriesRootPathSpec{})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						repos := []string{
 | 
				
			||||||
 | 
							"/foo/a/_layers/1",
 | 
				
			||||||
 | 
							"/foo/b/_layers/2",
 | 
				
			||||||
 | 
							"/bar/c/_layers/3",
 | 
				
			||||||
 | 
							"/bar/d/_layers/4",
 | 
				
			||||||
 | 
							"/foo/d/in/_layers/5",
 | 
				
			||||||
 | 
							"/an/invalid/repo",
 | 
				
			||||||
 | 
							"/bar/d/_layers/ignored/dir/6",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, repo := range repos {
 | 
				
			||||||
 | 
							if err := d.PutContent(ctx, rootpath+repo, c); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Unable to put to inmemory fs")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						expected := []string{
 | 
				
			||||||
 | 
							"bar/c",
 | 
				
			||||||
 | 
							"bar/d",
 | 
				
			||||||
 | 
							"foo/a",
 | 
				
			||||||
 | 
							"foo/b",
 | 
				
			||||||
 | 
							"foo/d/in",
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &setupEnv{
 | 
				
			||||||
 | 
							ctx:      ctx,
 | 
				
			||||||
 | 
							driver:   d,
 | 
				
			||||||
 | 
							expected: expected,
 | 
				
			||||||
 | 
							registry: registry,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCatalog(t *testing.T) {
 | 
				
			||||||
 | 
						env := setupFS(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						p := make([]string, 50)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						numFilled, err := env.registry.Repositories(env.ctx, p, "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !testEq(p, env.expected, numFilled) {
 | 
				
			||||||
 | 
							t.Errorf("Expected catalog repos err")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != io.EOF {
 | 
				
			||||||
 | 
							t.Errorf("Catalog has more values which we aren't expecting")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCatalogInParts(t *testing.T) {
 | 
				
			||||||
 | 
						env := setupFS(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						chunkLen := 2
 | 
				
			||||||
 | 
						p := make([]string, chunkLen)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						numFilled, err := env.registry.Repositories(env.ctx, p, "")
 | 
				
			||||||
 | 
						if err == io.EOF || numFilled != len(p) {
 | 
				
			||||||
 | 
							t.Errorf("Expected more values in catalog")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !testEq(p, env.expected[0:chunkLen], numFilled) {
 | 
				
			||||||
 | 
							t.Errorf("Expected catalog first chunk err")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lastRepo := p[len(p)-1]
 | 
				
			||||||
 | 
						numFilled, err = env.registry.Repositories(env.ctx, p, lastRepo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err == io.EOF || numFilled != len(p) {
 | 
				
			||||||
 | 
							t.Errorf("Expected more values in catalog")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !testEq(p, env.expected[chunkLen:chunkLen*2], numFilled) {
 | 
				
			||||||
 | 
							t.Errorf("Expected catalog second chunk err")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lastRepo = p[len(p)-1]
 | 
				
			||||||
 | 
						numFilled, err = env.registry.Repositories(env.ctx, p, lastRepo)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != io.EOF {
 | 
				
			||||||
 | 
							t.Errorf("Catalog has more values which we aren't expecting")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !testEq(p, env.expected[chunkLen*2:chunkLen*3-1], numFilled) {
 | 
				
			||||||
 | 
							t.Errorf("Expected catalog third chunk err")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func testEq(a, b []string, size int) bool {
 | 
				
			||||||
 | 
						for cnt := 0; cnt < size-1; cnt++ {
 | 
				
			||||||
 | 
							if a[cnt] != b[cnt] {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue