diff --git a/reference/reference.go b/reference/reference.go new file mode 100644 index 000000000..abf4d70d8 --- /dev/null +++ b/reference/reference.go @@ -0,0 +1,186 @@ +// Package reference provides a general type to represent any way of referencing images within the registry. +// Its main purpose is to abstract tags and digests (content-addressable hash). +// +// Grammar +// +// reference := repository [ ":" tag ] [ "@" digest ] +// +// // repository.go +// repository := hostname ['/' component]+ +// hostname := component [':' port-number] +// component := alpha-numeric [separator alpha-numeric]* +// alpha-numeric := /[a-zA-Z0-9]+/ +// separator := /[._-]/ +// port-number := /[0-9]+/ +// +// // tag.go +// tag := /[\w][\w.-]{0,127}/ +// +// // from the digest package +// digest := digest-algorithm ":" digest-hex +// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ] +// digest-algorithm-separator := /[+.-_]/ +// digest-algorithm-component := /[A-Za-z]/ /[A-Za-z0-9]*/ +// digest-hex := /[A-Za-z0-9_-]+/ ; supports hex bytes or url safe base64 +package reference + +import ( + "errors" + "regexp" + + "github.com/docker/distribution/digest" +) + +// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference. +var ErrReferenceInvalidFormat = errors.New("invalid reference format") + +// Reference abstracts types that reference images in a certain way. +type Reference interface { + // Repository returns the repository part of a reference + Repository() Repository + // String returns the entire reference, including the repository part + String() string +} + +func parseHostname(s string) (hostname, tail string) { + tail = s + i := regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String()).FindStringIndex(s) + if i == nil { + return + } + return s[:i[1]], s[i[1]:] +} + +func parseRepositoryName(s string) (repo, tail string) { + tail = s + i := regexp.MustCompile(`^/(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()).FindStringIndex(s) + if i == nil { + return + } + return s[:i[1]], s[i[1]:] +} + +func parseTag(s string) (tag Tag, tail string) { + tail = s + if len(s) == 0 || s[0] != ':' { + return + } + tag, err := NewTag(s[1:]) + if err != nil { + return + } + tail = s[len(tag)+1:] + return +} + +func parseDigest(s string) (dgst digest.Digest, tail string) { + tail = s + if len(s) == 0 || s[0] != '@' { + return + } + dgst, err := digest.ParseDigest(s[1:]) + if err != nil { + return + } + tail = s[len(dgst)+1:] + return +} + +// Parse parses s and returns a syntactically valid Reference. +// If an error was encountered it is returned, along with a nil Reference. +func Parse(s string) (Reference, error) { + hostname, s := parseHostname(s) + name, s := parseRepositoryName(s) + repository := Repository{Hostname: hostname, Name: name} + if err := repository.Validate(); err != nil { + return nil, err + } + tag, s := parseTag(s) + dgst, s := parseDigest(s) + if len(s) > 0 { + return nil, ErrReferenceInvalidFormat + } + + if dgst != "" { + return DigestReference{repository: repository, digest: dgst, tag: tag}, nil + } + if tag != "" { + return TagReference{repository: repository, tag: tag}, nil + } + return nil, ErrReferenceInvalidFormat +} + +// DigestReference represents a reference of the form `repository@sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef`. +// Implements the Reference interface. +type DigestReference struct { + repository Repository + digest digest.Digest + tag Tag +} + +// Repository returns the repository part. +func (r DigestReference) Repository() Repository { return r.repository } + +// String returns the full string reference. +func (r DigestReference) String() string { + return r.repository.String() + "@" + string(r.digest) +} + +// NewDigestReference returns an initialized DigestReference. +func NewDigestReference(canonicalRepository string, digest digest.Digest, optionalTag Tag) (DigestReference, error) { + ref := DigestReference{} + + repo, err := NewRepository(canonicalRepository) + if err != nil { + return ref, err + } + ref.repository = repo + + if err := digest.Validate(); err != nil { + return ref, err + } + ref.digest = digest + + if len(optionalTag) > 0 { + if err := optionalTag.Validate(); err != nil { + return ref, err + } + ref.tag = optionalTag + } + + return ref, err +} + +// TagReference represents a reference of the form `repository:tag`. +// Implements the Reference interface. +type TagReference struct { + repository Repository + tag Tag +} + +// Repository returns the repository part. +func (r TagReference) Repository() Repository { return r.repository } + +// String returns the full string reference. +func (r TagReference) String() string { + return r.repository.String() + ":" + string(r.tag) +} + +// NewTagReference returns an initialized TagReference. +func NewTagReference(canonicalRepository string, tagName string) (TagReference, error) { + ref := TagReference{} + + repo, err := NewRepository(canonicalRepository) + if err != nil { + return ref, err + } + ref.repository = repo + + tag, err := NewTag(tagName) + if err != nil { + return ref, err + } + ref.tag = tag + + return ref, err +} diff --git a/reference/reference_test.go b/reference/reference_test.go new file mode 100644 index 000000000..7af84f330 --- /dev/null +++ b/reference/reference_test.go @@ -0,0 +1,56 @@ +package reference + +/* +var refRegex = regexp.MustCompile(`^([a-z0-9]+(?:[-._][a-z0-9]+)*(?::[0-9]+(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+|(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+)?)(:[\w][\w.-]{0,127})?(@` + digest.DigestRegexp.String() + `)?$`) + +func getRepo(s string) string { + matches := refRegex.FindStringSubmatch(s) + if len(matches) == 0 { + return "" + } + return matches[1] +} + +func testRepository(prefix string) error { + for _, s := range []string{ + prefix + `@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, + prefix + `:frozen@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, + prefix + `:latest`, + prefix, + } { + expected := getRepo(s) + ref, err := Parse(s) + if err != nil { + if expected == "" { + continue + } + return err + } + if repo := ref.Repository(); repo.String() != expected { + return fmt.Errorf("repository string: expected %q, got: %q", expected, repo) + } + if refStr := ref.String(); refStr != s { + return fmt.Errorf("reference string: expected %q, got: %q", s, refStr) + } + } + return nil +} + +func TestSimpleRepository(t *testing.T) { + if err := testRepository(`busybox`); err != nil { + t.Fatal(err) + } +} + +func TestUrlRepository(t *testing.T) { + if err := testRepository(`docker.io/library/busybox`); err != nil { + t.Fatal(err) + } +} + +func TestPort(t *testing.T) { + if err := testRepository(`busybox:1234`); err != nil { + t.Fatal(err) + } +} +*/ diff --git a/reference/repository.go b/reference/repository.go new file mode 100644 index 000000000..936b09297 --- /dev/null +++ b/reference/repository.go @@ -0,0 +1,136 @@ +package reference + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +const ( + // RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name. + RepositoryNameTotalLengthMax = 255 +) + +// RepositoryNameComponentRegexp restricts registry path component names to +// start with at least one letter or number, with following parts able to +// be separated by one period, dash or underscore. +var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`) + +// RepositoryNameComponentAnchoredRegexp is the version of +// RepositoryNameComponentRegexp which must completely match the content +var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`) + +// RepositoryNameHostnameRegexp restricts the registry hostname component of a repository name to +// start with a component as defined by RepositoryNameComponentRegexp and followed by an optional port. +var RepositoryNameHostnameRegexp = regexp.MustCompile(RepositoryNameComponentRegexp.String() + `(?::[0-9]+)?`) + +// RepositoryNameHostnameAnchoredRegexp is the version of +// RepositoryNameHostnameRegexp which must completely match the content. +var RepositoryNameHostnameAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String() + `$`) + +// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow +// multiple path components, separated by a forward slash. +var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameHostnameRegexp.String() + `/)?(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()) + +var ( + // ErrRepositoryNameEmpty is returned for empty, invalid repository names. + ErrRepositoryNameEmpty = errors.New("repository name must have at least one component") + + // ErrRepositoryNameMissingHostname is returned when a repository name + // does not start with a hostname + ErrRepositoryNameMissingHostname = errors.New("repository name must start with a hostname") + + // ErrRepositoryNameHostnameInvalid is returned when a repository name + // does not match RepositoryNameHostnameRegexp + ErrRepositoryNameHostnameInvalid = fmt.Errorf("repository name must match %q", RepositoryNameHostnameRegexp.String()) + + // 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()) +) + +// Repository represents a reference to a Repository. +type Repository struct { + // Hostname refers to the registry hostname where the repository resides. + Hostname string + // Name is a slash (`/`) separated list of string components. + Name string +} + +// String returns the string representation of a repository. +func (r Repository) String() string { + // Hostname is not supposed to be empty, but let's be nice. + if len(r.Hostname) == 0 { + return r.Name + } + return r.Hostname + "/" + r.Name +} + +// Validate 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: +// +// repository := hostname ['/' component]+ +// hostname := component [':' port-number] +// component := alpha-numeric [separator alpha-numeric]* +// alpha-numeric := /[a-zA-Z0-9]+/ +// separator := /[._-]/ +// port-number := /[0-9]+/ +// +// The result of the production should be limited to 255 characters. +func (r Repository) Validate() error { + n := len(r.String()) + switch { + case n == 0: + return ErrRepositoryNameEmpty + case n > RepositoryNameTotalLengthMax: + return ErrRepositoryNameLong + case len(r.Hostname) <= 0: + return ErrRepositoryNameMissingHostname + case !RepositoryNameHostnameAnchoredRegexp.MatchString(r.Hostname): + return ErrRepositoryNameHostnameInvalid + } + + components := r.Name + for { + var component string + sep := strings.Index(components, "/") + if sep >= 0 { + component = components[:sep] + components = components[sep+1:] + } else { // if no more slashes + component = components + components = "" + } + if !RepositoryNameComponentAnchoredRegexp.MatchString(component) { + return ErrRepositoryNameComponentInvalid + } + if sep < 0 { + return nil + } + } +} + +// NewRepository returns a valid Repository from an input string representing +// the canonical form of a repository name. +// If the validation fails, an error is returned. +func NewRepository(canonicalName string) (repo Repository, err error) { + if len(canonicalName) == 0 { + return repo, ErrRepositoryNameEmpty + } + i := strings.Index(canonicalName, "/") + if i <= 0 { + return repo, ErrRepositoryNameMissingHostname + } + repo.Hostname = canonicalName[:i] + repo.Name = canonicalName[i+1:] + return repo, repo.Validate() +} diff --git a/registry/api/v2/names_test.go b/reference/repository_test.go similarity index 74% rename from registry/api/v2/names_test.go rename to reference/repository_test.go index f4daf2e7f..67d65f9d8 100644 --- a/registry/api/v2/names_test.go +++ b/reference/repository_test.go @@ -1,6 +1,7 @@ -package v2 +package reference import ( + "regexp" "strconv" "strings" "testing" @@ -20,11 +21,13 @@ var ( invalid bool }{ { - input: "", - err: ErrRepositoryNameEmpty, + input: "", + err: ErrRepositoryNameEmpty, + invalid: true, }, { input: "short", + err: ErrRepositoryNameMissingHostname, }, { input: "simple/name", @@ -56,6 +59,7 @@ var ( }, { input: "a", + err: ErrRepositoryNameMissingHostname, }, { input: "a/aa", @@ -72,11 +76,7 @@ var ( 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:8080/bar", }, { input: "foo.com/bar", @@ -92,10 +92,16 @@ var ( }, { input: "asdf", + err: ErrRepositoryNameMissingHostname, + }, + { + input: "aa/asdf$$^/aa", + err: ErrRepositoryNameComponentInvalid, + invalid: true, }, { input: "asdf$$^/aa", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { @@ -107,21 +113,35 @@ var ( { input: "a-a/a-a", }, + { + input: "a", + err: ErrRepositoryNameMissingHostname, + }, + { + input: "a/image", + }, { input: "a-/a/a/a", + err: ErrRepositoryNameHostnameInvalid, + invalid: true, + }, + { + input: "a/a-/a/a/a", err: ErrRepositoryNameComponentInvalid, invalid: true, }, { - input: strings.Repeat("a", 255), + // total length = 255 + input: "a/" + strings.Repeat("a", 253), }, { - input: strings.Repeat("a", 256), + // total length = 256 + input: "b/" + strings.Repeat("a", 254), err: ErrRepositoryNameLong, }, { input: "-foo/bar", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { @@ -131,7 +151,7 @@ var ( }, { input: "foo-/bar", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { @@ -141,7 +161,7 @@ var ( }, { input: "_foo/bar", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { @@ -151,17 +171,17 @@ var ( }, { input: "____/____", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { input: "_docker/_docker", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { input: "docker_/docker_", - err: ErrRepositoryNameComponentInvalid, + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { @@ -190,8 +210,17 @@ var ( invalid: true, }, { - input: "-docker/docker", - err: ErrRepositoryNameComponentInvalid, + input: "-docker/docker", + err: ErrRepositoryNameComponentInvalid, + }, + { + input: "xn--n3h.com/myimage", // http://☃.com in punycode + err: ErrRepositoryNameHostnameInvalid, + invalid: true, + }, + { + input: "xn--7o8h.com/myimage", // http://🐳.com in punycode + err: ErrRepositoryNameHostnameInvalid, invalid: true, }, { @@ -218,7 +247,7 @@ func TestValidateRepositoryName(t *testing.T) { t.Fail() } - if err := ValidateRepositoryName(testcase.input); err != testcase.err { + if _, err := NewRepository(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) @@ -238,13 +267,14 @@ func TestValidateRepositoryName(t *testing.T) { } func TestRepositoryNameRegexp(t *testing.T) { + AnchoredRepositoryNameRegexp := regexp.MustCompile(`^` + RepositoryNameRegexp.String() + `$`) 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 + matches := AnchoredRepositoryNameRegexp.MatchString(testcase.input) if matches == testcase.invalid { if testcase.invalid { failf("expected invalid repository name %s", testcase.input) diff --git a/reference/tag.go b/reference/tag.go new file mode 100644 index 000000000..8deee36e8 --- /dev/null +++ b/reference/tag.go @@ -0,0 +1,38 @@ +package reference + +import ( + "fmt" + "regexp" +) + +var ( + // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. + TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`) + + // TagAnchoredRegexp matches valid tag names, anchored at the start and + // end of the matched string. + TagAnchoredRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) + + // ErrTagInvalid is returned when a tag does not match TagAnchoredRegexp. + ErrTagInvalid = fmt.Errorf("tag name must match %q", TagRegexp.String()) +) + +// Tag represents an image's tag name. +type Tag string + +// NewTag returns a valid Tag from an input string s. +// If the validation fails, an error is returned. +func NewTag(s string) (Tag, error) { + tag := Tag(s) + return tag, tag.Validate() +} + +// Validate returns ErrTagInvalid if tag does not match TagAnchoredRegexp. +// +// tag := [\w][\w.-]{0,127} +func (tag Tag) Validate() error { + if !TagAnchoredRegexp.MatchString(string(tag)) { + return ErrTagInvalid + } + return nil +} diff --git a/registry/api/v2/descriptors.go b/registry/api/v2/descriptors.go index c5630fed2..ef37997a3 100644 --- a/registry/api/v2/descriptors.go +++ b/registry/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/registry/api/v2/names.go b/registry/api/v2/names.go deleted file mode 100644 index 5f340793c..000000000 --- a/registry/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/registry/client/repository.go b/registry/client/repository.go index 1e189438f..db45a4647 100644 --- a/registry/client/repository.go +++ b/registry/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/registry/storage/cache/memory/memory.go b/registry/storage/cache/memory/memory.go index 120a6572d..725a68e71 100644 --- a/registry/storage/cache/memory/memory.go +++ b/registry/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/registry/storage/cache/redis/redis.go b/registry/storage/cache/redis/redis.go index 36370bdd9..54138f3df 100644 --- a/registry/storage/cache/redis/redis.go +++ b/registry/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/registry/storage/registry.go b/registry/storage/registry.go index 0b38ea9b0..e3b132c52 100644 --- a/registry/storage/registry.go +++ b/registry/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 }