Add a new reference package abstracting repositories, tags and digests

There seems to be a need for a type that represents a way of pointing
to an image, irrespective of the implementation.

This patch defines a Reference interface and provides 3 implementations:
- TagReference: when only a tag is provided
- DigestReference: when a digest (according to the digest package) is
  provided, can include optional tag as well

Validation of references are purely syntactic.

There is also a strong type for tags, analogous to digests, as well
as a strong type for Repository from which clients can access the
hostname alone, or the repository name without the hostname, or both
together via the String() method.

For Repository, the files names.go and names_test.go were moved from
the v2 package.

Signed-off-by: Tibor Vass <tibor@docker.com>
This commit is contained in:
Tibor Vass 2015-07-10 14:36:04 -04:00 committed by Derek McGowan
parent 1d266b00e1
commit f8c09b6a7d
11 changed files with 495 additions and 143 deletions

186
reference/reference.go Normal file
View file

@ -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
}

View file

@ -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)
}
}
*/

136
reference/repository.go Normal file
View file

@ -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()
}

View file

@ -1,6 +1,7 @@
package v2 package reference
import ( import (
"regexp"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
@ -20,11 +21,13 @@ var (
invalid bool invalid bool
}{ }{
{ {
input: "", input: "",
err: ErrRepositoryNameEmpty, err: ErrRepositoryNameEmpty,
invalid: true,
}, },
{ {
input: "short", input: "short",
err: ErrRepositoryNameMissingHostname,
}, },
{ {
input: "simple/name", input: "simple/name",
@ -56,6 +59,7 @@ var (
}, },
{ {
input: "a", input: "a",
err: ErrRepositoryNameMissingHostname,
}, },
{ {
input: "a/aa", input: "a/aa",
@ -72,11 +76,7 @@ var (
invalid: true, invalid: true,
}, },
{ {
// TODO: this testcase should be valid once we switch to input: "foo.com:8080/bar",
// the reference package.
input: "foo.com:8080/bar",
err: ErrRepositoryNameComponentInvalid,
invalid: true,
}, },
{ {
input: "foo.com/bar", input: "foo.com/bar",
@ -92,10 +92,16 @@ var (
}, },
{ {
input: "asdf", input: "asdf",
err: ErrRepositoryNameMissingHostname,
},
{
input: "aa/asdf$$^/aa",
err: ErrRepositoryNameComponentInvalid,
invalid: true,
}, },
{ {
input: "asdf$$^/aa", input: "asdf$$^/aa",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
@ -107,21 +113,35 @@ var (
{ {
input: "a-a/a-a", input: "a-a/a-a",
}, },
{
input: "a",
err: ErrRepositoryNameMissingHostname,
},
{
input: "a/image",
},
{ {
input: "a-/a/a/a", input: "a-/a/a/a",
err: ErrRepositoryNameHostnameInvalid,
invalid: true,
},
{
input: "a/a-/a/a/a",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameComponentInvalid,
invalid: true, 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, err: ErrRepositoryNameLong,
}, },
{ {
input: "-foo/bar", input: "-foo/bar",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
@ -131,7 +151,7 @@ var (
}, },
{ {
input: "foo-/bar", input: "foo-/bar",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
@ -141,7 +161,7 @@ var (
}, },
{ {
input: "_foo/bar", input: "_foo/bar",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
@ -151,17 +171,17 @@ var (
}, },
{ {
input: "____/____", input: "____/____",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
input: "_docker/_docker", input: "_docker/_docker",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
input: "docker_/docker_", input: "docker_/docker_",
err: ErrRepositoryNameComponentInvalid, err: ErrRepositoryNameHostnameInvalid,
invalid: true, invalid: true,
}, },
{ {
@ -190,8 +210,17 @@ var (
invalid: true, invalid: true,
}, },
{ {
input: "-docker/docker", input: "-docker/docker",
err: ErrRepositoryNameComponentInvalid, 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, invalid: true,
}, },
{ {
@ -218,7 +247,7 @@ func TestValidateRepositoryName(t *testing.T) {
t.Fail() t.Fail()
} }
if err := ValidateRepositoryName(testcase.input); err != testcase.err { if _, err := NewRepository(testcase.input); err != testcase.err {
if testcase.err != nil { if testcase.err != nil {
if err != nil { if err != nil {
failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err) 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) { func TestRepositoryNameRegexp(t *testing.T) {
AnchoredRepositoryNameRegexp := regexp.MustCompile(`^` + RepositoryNameRegexp.String() + `$`)
for _, testcase := range regexpTestcases { for _, testcase := range regexpTestcases {
failf := func(format string, v ...interface{}) { failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...) t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail() t.Fail()
} }
matches := RepositoryNameRegexp.FindString(testcase.input) == testcase.input matches := AnchoredRepositoryNameRegexp.MatchString(testcase.input)
if matches == testcase.invalid { if matches == testcase.invalid {
if testcase.invalid { if testcase.invalid {
failf("expected invalid repository name %s", testcase.input) failf("expected invalid repository name %s", testcase.input)

38
reference/tag.go Normal file
View file

@ -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
}

View file

@ -5,6 +5,7 @@ import (
"regexp" "regexp"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/errcode"
) )
@ -12,7 +13,7 @@ var (
nameParameterDescriptor = ParameterDescriptor{ nameParameterDescriptor = ParameterDescriptor{
Name: "name", Name: "name",
Type: "string", Type: "string",
Format: RepositoryNameRegexp.String(), Format: reference.RepositoryNameRegexp.String(),
Required: true, Required: true,
Description: `Name of the target repository.`, Description: `Name of the target repository.`,
} }
@ -20,7 +21,7 @@ var (
referenceParameterDescriptor = ParameterDescriptor{ referenceParameterDescriptor = ParameterDescriptor{
Name: "reference", Name: "reference",
Type: "string", Type: "string",
Format: TagNameRegexp.String(), Format: reference.TagRegexp.String(),
Required: true, Required: true,
Description: `Tag or digest of the target manifest.`, Description: `Tag or digest of the target manifest.`,
} }
@ -389,7 +390,7 @@ var routeDescriptors = []RouteDescriptor{
}, },
{ {
Name: RouteNameTags, Name: RouteNameTags,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list", Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/tags/list",
Entity: "Tags", Entity: "Tags",
Description: "Retrieve information about tags.", Description: "Retrieve information about tags.",
Methods: []MethodDescriptor{ Methods: []MethodDescriptor{
@ -517,7 +518,7 @@ var routeDescriptors = []RouteDescriptor{
}, },
{ {
Name: RouteNameManifest, 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", Entity: "Manifest",
Description: "Create, update, delete and retrieve manifests.", Description: "Create, update, delete and retrieve manifests.",
Methods: []MethodDescriptor{ Methods: []MethodDescriptor{
@ -782,7 +783,7 @@ var routeDescriptors = []RouteDescriptor{
{ {
Name: RouteNameBlob, 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", Entity: "Blob",
Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.",
Methods: []MethodDescriptor{ Methods: []MethodDescriptor{
@ -1006,7 +1007,7 @@ var routeDescriptors = []RouteDescriptor{
{ {
Name: RouteNameBlobUpload, Name: RouteNameBlobUpload,
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/", Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/uploads/",
Entity: "Initiate Blob Upload", Entity: "Initiate Blob Upload",
Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.", Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.",
Methods: []MethodDescriptor{ Methods: []MethodDescriptor{
@ -1128,7 +1129,7 @@ var routeDescriptors = []RouteDescriptor{
{ {
Name: RouteNameBlobUploadChunk, 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", 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.", 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{ Methods: []MethodDescriptor{

View file

@ -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
}

View file

@ -15,6 +15,7 @@ import (
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "github.com/docker/distribution/digest"
"github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema1"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/api/v2"
"github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/client/transport"
"github.com/docker/distribution/registry/storage/cache" "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 return numFilled, returnErr
} }
// NewRepository creates a new Repository for the given repository name and base URL // NewRepository creates a new Repository for the given canonical repository name and base URL.
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { func NewRepository(ctx context.Context, canonicalName, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
if err := v2.ValidateRepositoryName(name); err != nil { if _, err := reference.NewRepository(canonicalName); err != nil {
return nil, err return nil, err
} }
@ -115,7 +116,7 @@ func NewRepository(ctx context.Context, name, baseURL string, transport http.Rou
return &repository{ return &repository{
client: client, client: client,
ub: ub, ub: ub,
name: name, name: canonicalName,
context: ctx, context: ctx,
}, nil }, nil
} }

View file

@ -6,7 +6,7 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "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/docker/distribution/registry/storage/cache"
) )
@ -25,8 +25,8 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider
} }
} }
func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) {
if err := v2.ValidateRepositoryName(repo); err != nil { if _, err := reference.NewRepository(canonicalName); err != nil {
return nil, err return nil, err
} }
@ -34,9 +34,9 @@ func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string)
defer imbdcp.mu.RUnlock() defer imbdcp.mu.RUnlock()
return &repositoryScopedInMemoryBlobDescriptorCache{ return &repositoryScopedInMemoryBlobDescriptorCache{
repo: repo, repo: canonicalName,
parent: imbdcp, parent: imbdcp,
repository: imbdcp.repositories[repo], repository: imbdcp.repositories[canonicalName],
}, nil }, nil
} }

View file

@ -6,7 +6,7 @@ import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/digest" "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/docker/distribution/registry/storage/cache"
"github.com/garyburd/redigo/redis" "github.com/garyburd/redigo/redis"
) )
@ -40,13 +40,13 @@ func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorC
} }
// RepositoryScoped returns the scoped cache. // RepositoryScoped returns the scoped cache.
func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { func (rbds *redisBlobDescriptorService) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) {
if err := v2.ValidateRepositoryName(repo); err != nil { if _, err := reference.NewRepository(canonicalName); err != nil {
return nil, err return nil, err
} }
return &repositoryScopedRedisBlobDescriptorService{ return &repositoryScopedRedisBlobDescriptorService{
repo: repo, repo: canonicalName,
upstream: rbds, upstream: rbds,
}, nil }, nil
} }

View file

@ -3,7 +3,7 @@ package storage
import ( import (
"github.com/docker/distribution" "github.com/docker/distribution"
"github.com/docker/distribution/context" "github.com/docker/distribution/context"
"github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/storage/cache" "github.com/docker/distribution/registry/storage/cache"
storagedriver "github.com/docker/distribution/registry/storage/driver" 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. // Repository returns an instance of the repository tied to the registry.
// Instances should not be shared between goroutines but are cheap to // Instances should not be shared between goroutines but are cheap to
// allocate. In general, they should be request scoped. // allocate. In general, they should be request scoped.
func (reg *registry) Repository(ctx context.Context, name string) (distribution.Repository, error) { func (reg *registry) Repository(ctx context.Context, canonicalName string) (distribution.Repository, error) {
if err := v2.ValidateRepositoryName(name); err != nil { if _, err := reference.NewRepository(canonicalName); err != nil {
return nil, distribution.ErrRepositoryNameInvalid{ return nil, distribution.ErrRepositoryNameInvalid{
Name: name, Name: canonicalName,
Reason: err, Reason: err,
} }
} }
@ -118,7 +118,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
var descriptorCache distribution.BlobDescriptorService var descriptorCache distribution.BlobDescriptorService
if reg.blobDescriptorCacheProvider != nil { if reg.blobDescriptorCacheProvider != nil {
var err error var err error
descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(name) descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(canonicalName)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -127,7 +127,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
return &repository{ return &repository{
ctx: ctx, ctx: ctx,
registry: reg, registry: reg,
name: name, name: canonicalName,
descriptorCache: descriptorCache, descriptorCache: descriptorCache,
}, nil }, nil
} }