diff --git a/docs/api/v2/descriptors.go b/docs/api/v2/descriptors.go index c5630fed..ef37997a 100644 --- a/docs/api/v2/descriptors.go +++ b/docs/api/v2/descriptors.go @@ -5,6 +5,7 @@ import ( "regexp" "github.com/docker/distribution/digest" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" ) @@ -12,7 +13,7 @@ var ( nameParameterDescriptor = ParameterDescriptor{ Name: "name", Type: "string", - Format: RepositoryNameRegexp.String(), + Format: reference.RepositoryNameRegexp.String(), Required: true, Description: `Name of the target repository.`, } @@ -20,7 +21,7 @@ var ( referenceParameterDescriptor = ParameterDescriptor{ Name: "reference", Type: "string", - Format: TagNameRegexp.String(), + Format: reference.TagRegexp.String(), Required: true, Description: `Tag or digest of the target manifest.`, } @@ -389,7 +390,7 @@ var routeDescriptors = []RouteDescriptor{ }, { Name: RouteNameTags, - Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list", + Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/tags/list", Entity: "Tags", Description: "Retrieve information about tags.", Methods: []MethodDescriptor{ @@ -517,7 +518,7 @@ var routeDescriptors = []RouteDescriptor{ }, { Name: RouteNameManifest, - Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + digest.DigestRegexp.String() + "}", + Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", Entity: "Manifest", Description: "Create, update, delete and retrieve manifests.", Methods: []MethodDescriptor{ @@ -782,7 +783,7 @@ var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBlob, - Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", + Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", Entity: "Blob", Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", Methods: []MethodDescriptor{ @@ -1006,7 +1007,7 @@ var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBlobUpload, - Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/", + Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/uploads/", Entity: "Initiate Blob Upload", Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.", Methods: []MethodDescriptor{ @@ -1128,7 +1129,7 @@ var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBlobUploadChunk, - Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}", + Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}", Entity: "Blob Upload", Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.", Methods: []MethodDescriptor{ diff --git a/docs/api/v2/names.go b/docs/api/v2/names.go deleted file mode 100644 index 5f340793..00000000 --- a/docs/api/v2/names.go +++ /dev/null @@ -1,96 +0,0 @@ -package v2 - -import ( - "fmt" - "regexp" - "strings" -) - -// TODO(stevvooe): Move these definitions to the future "reference" package. -// While they are used with v2 definitions, their relevance expands beyond. - -const ( - // RepositoryNameTotalLengthMax is the maximum total number of characters in - // a repository name - RepositoryNameTotalLengthMax = 255 -) - -// domainLabelRegexp represents the following RFC-2396 BNF construct: -// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum -var domainLabelRegexp = regexp.MustCompile(`[a-z0-9](?:-*[a-z0-9])*`) - -// RepositoryNameComponentRegexp restricts registry path component names to -// the allow valid hostnames according to: https://www.ietf.org/rfc/rfc2396.txt -// with the following differences: -// 1) It DOES NOT allow for fully-qualified domain names, which include a -// trailing '.', e.g. "google.com." -// 2) It DOES NOT restrict 'top-level' domain labels to start with just alpha -// characters. -// 3) It DOES allow for underscores to appear in the same situations as dots. -// -// RFC-2396 uses the BNF construct: -// hostname = *( domainlabel "." ) toplabel [ "." ] -var RepositoryNameComponentRegexp = regexp.MustCompile( - domainLabelRegexp.String() + `(?:[._]` + domainLabelRegexp.String() + `)*`) - -// RepositoryNameComponentAnchoredRegexp is the version of -// RepositoryNameComponentRegexp which must completely match the content -var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`) - -// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow -// multiple path components, separated by a forward slash. -var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()) - -// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go. -var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`) - -// TagNameAnchoredRegexp matches valid tag names, anchored at the start and -// end of the matched string. -var TagNameAnchoredRegexp = regexp.MustCompile("^" + TagNameRegexp.String() + "$") - -var ( - // ErrRepositoryNameEmpty is returned for empty, invalid repository names. - ErrRepositoryNameEmpty = fmt.Errorf("repository name must have at least one component") - - // ErrRepositoryNameLong is returned when a repository name is longer than - // RepositoryNameTotalLengthMax - ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax) - - // ErrRepositoryNameComponentInvalid is returned when a repository name does - // not match RepositoryNameComponentRegexp - ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String()) -) - -// ValidateRepositoryName ensures the repository name is valid for use in the -// registry. This function accepts a superset of what might be accepted by -// docker core or docker hub. If the name does not pass validation, an error, -// describing the conditions, is returned. -// -// Effectively, the name should comply with the following grammar: -// -// alpha-numeric := /[a-z0-9]+/ -// separator := /[._-]/ -// component := alpha-numeric [separator alpha-numeric]* -// namespace := component ['/' component]* -// -// The result of the production, known as the "namespace", should be limited -// to 255 characters. -func ValidateRepositoryName(name string) error { - if name == "" { - return ErrRepositoryNameEmpty - } - - if len(name) > RepositoryNameTotalLengthMax { - return ErrRepositoryNameLong - } - - components := strings.Split(name, "/") - - for _, component := range components { - if !RepositoryNameComponentAnchoredRegexp.MatchString(component) { - return ErrRepositoryNameComponentInvalid - } - } - - return nil -} diff --git a/docs/api/v2/names_test.go b/docs/api/v2/names_test.go deleted file mode 100644 index f4daf2e7..00000000 --- a/docs/api/v2/names_test.go +++ /dev/null @@ -1,256 +0,0 @@ -package v2 - -import ( - "strconv" - "strings" - "testing" -) - -var ( - // regexpTestcases is a unified set of testcases for - // TestValidateRepositoryName and TestRepositoryNameRegexp. - // Some of them are valid inputs for one and not the other. - regexpTestcases = []struct { - // input is the repository name or name component testcase - input string - // err is the error expected from ValidateRepositoryName, or nil - err error - // invalid should be true if the testcase is *not* expected to - // match RepositoryNameRegexp - invalid bool - }{ - { - input: "", - err: ErrRepositoryNameEmpty, - }, - { - input: "short", - }, - { - input: "simple/name", - }, - { - input: "library/ubuntu", - }, - { - input: "docker/stevvooe/app", - }, - { - input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", - }, - { - input: "aa/aa/bb/bb/bb", - }, - { - input: "a/a/a/b/b", - }, - { - input: "a/a/a/a/", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "a//a/a", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "a", - }, - { - input: "a/aa", - }, - { - input: "aa/a", - }, - { - input: "a/aa/a", - }, - { - input: "foo.com/", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - // TODO: this testcase should be valid once we switch to - // the reference package. - input: "foo.com:8080/bar", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo.com/bar", - }, - { - input: "foo.com/bar/baz", - }, - { - input: "foo.com/bar/baz/quux", - }, - { - input: "blog.foo.com/bar/baz", - }, - { - input: "asdf", - }, - { - input: "asdf$$^/aa", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "aa-a/aa", - }, - { - input: "aa/aa", - }, - { - input: "a-a/a-a", - }, - { - input: "a-/a/a/a", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: strings.Repeat("a", 255), - }, - { - input: strings.Repeat("a", 256), - err: ErrRepositoryNameLong, - }, - { - input: "-foo/bar", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo/bar-", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo-/bar", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo/-bar", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "_foo/bar", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo/bar_", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "____/____", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "_docker/_docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "docker_/docker_", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "do__cker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "docker./docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: ".docker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "do..cker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "docker-/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "-docker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "b.gcr.io/test.example.com/my-app", // embedded domain component - }, - { - input: "xn--n3h.com/myimage", // http://☃.com in punycode - }, - { - input: "xn--7o8h.com/myimage", // http://🐳.com in punycode - }, - { - input: "registry.io/foo/project--id.module--name.ver---sion--name", // image with hostname - }, - } -) - -// TestValidateRepositoryName tests the ValidateRepositoryName function, -// which uses RepositoryNameComponentAnchoredRegexp for validation -func TestValidateRepositoryName(t *testing.T) { - for _, testcase := range regexpTestcases { - failf := func(format string, v ...interface{}) { - t.Logf(strconv.Quote(testcase.input)+": "+format, v...) - t.Fail() - } - - if err := ValidateRepositoryName(testcase.input); err != testcase.err { - if testcase.err != nil { - if err != nil { - failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err) - } else { - failf("expected invalid repository: %v", testcase.err) - } - } else { - if err != nil { - // Wrong error returned. - failf("unexpected error validating repository name: %v, expected %v", err, testcase.err) - } else { - failf("unexpected error validating repository name: %v", err) - } - } - } - } -} - -func TestRepositoryNameRegexp(t *testing.T) { - for _, testcase := range regexpTestcases { - failf := func(format string, v ...interface{}) { - t.Logf(strconv.Quote(testcase.input)+": "+format, v...) - t.Fail() - } - - matches := RepositoryNameRegexp.FindString(testcase.input) == testcase.input - if matches == testcase.invalid { - if testcase.invalid { - failf("expected invalid repository name %s", testcase.input) - } else { - failf("expected valid repository name %s", testcase.input) - } - } - } -} diff --git a/docs/client/repository.go b/docs/client/repository.go index 1e189438..db45a464 100644 --- a/docs/client/repository.go +++ b/docs/client/repository.go @@ -15,6 +15,7 @@ import ( "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema1" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/storage/cache" @@ -96,9 +97,9 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri 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 { +// NewRepository creates a new Repository for the given canonical repository name and base URL. +func NewRepository(ctx context.Context, canonicalName, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { + if _, err := reference.NewRepository(canonicalName); err != nil { return nil, err } @@ -115,7 +116,7 @@ func NewRepository(ctx context.Context, name, baseURL string, transport http.Rou return &repository{ client: client, ub: ub, - name: name, + name: canonicalName, context: ctx, }, nil } diff --git a/docs/storage/cache/memory/memory.go b/docs/storage/cache/memory/memory.go index 120a6572..725a68e7 100644 --- a/docs/storage/cache/memory/memory.go +++ b/docs/storage/cache/memory/memory.go @@ -6,7 +6,7 @@ import ( "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" - "github.com/docker/distribution/registry/api/v2" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache" ) @@ -25,8 +25,8 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider } } -func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { - if err := v2.ValidateRepositoryName(repo); err != nil { +func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) { + if _, err := reference.NewRepository(canonicalName); err != nil { return nil, err } @@ -34,9 +34,9 @@ func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) defer imbdcp.mu.RUnlock() return &repositoryScopedInMemoryBlobDescriptorCache{ - repo: repo, + repo: canonicalName, parent: imbdcp, - repository: imbdcp.repositories[repo], + repository: imbdcp.repositories[canonicalName], }, nil } diff --git a/docs/storage/cache/redis/redis.go b/docs/storage/cache/redis/redis.go index 36370bdd..54138f3d 100644 --- a/docs/storage/cache/redis/redis.go +++ b/docs/storage/cache/redis/redis.go @@ -6,7 +6,7 @@ import ( "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" - "github.com/docker/distribution/registry/api/v2" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache" "github.com/garyburd/redigo/redis" ) @@ -40,13 +40,13 @@ func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorC } // RepositoryScoped returns the scoped cache. -func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { - if err := v2.ValidateRepositoryName(repo); err != nil { +func (rbds *redisBlobDescriptorService) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) { + if _, err := reference.NewRepository(canonicalName); err != nil { return nil, err } return &repositoryScopedRedisBlobDescriptorService{ - repo: repo, + repo: canonicalName, upstream: rbds, }, nil } diff --git a/docs/storage/registry.go b/docs/storage/registry.go index 0b38ea9b..e3b132c5 100644 --- a/docs/storage/registry.go +++ b/docs/storage/registry.go @@ -3,7 +3,7 @@ package storage import ( "github.com/docker/distribution" "github.com/docker/distribution/context" - "github.com/docker/distribution/registry/api/v2" + "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache" storagedriver "github.com/docker/distribution/registry/storage/driver" ) @@ -107,10 +107,10 @@ func (reg *registry) Scope() distribution.Scope { // Repository returns an instance of the repository tied to the registry. // Instances should not be shared between goroutines but are cheap to // allocate. In general, they should be request scoped. -func (reg *registry) Repository(ctx context.Context, name string) (distribution.Repository, error) { - if err := v2.ValidateRepositoryName(name); err != nil { +func (reg *registry) Repository(ctx context.Context, canonicalName string) (distribution.Repository, error) { + if _, err := reference.NewRepository(canonicalName); err != nil { return nil, distribution.ErrRepositoryNameInvalid{ - Name: name, + Name: canonicalName, Reason: err, } } @@ -118,7 +118,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution. var descriptorCache distribution.BlobDescriptorService if reg.blobDescriptorCacheProvider != nil { var err error - descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(name) + descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(canonicalName) if err != nil { return nil, err } @@ -127,7 +127,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution. return &repository{ ctx: ctx, registry: reg, - name: name, + name: canonicalName, descriptorCache: descriptorCache, }, nil }