Split apart repository reference into domain and path

Allows having other parsers which are capable of unambiguously keeping domain and path separated in a Reference type.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
Derek McGowan 2016-06-09 11:32:23 -07:00
parent 76f514b618
commit 9a43b8f696
No known key found for this signature in database
GPG key ID: F58C5D0A4405ACDB
4 changed files with 169 additions and 104 deletions

View file

@ -4,11 +4,11 @@
// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// name := [hostname '/'] component ['/' component]*
// hostname := hostcomponent ['.' hostcomponent]* [':' port-number]
// hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// name := [domain '/'] path-component ['/' path-component]*
// domain := domain-component ['.' domain-component]* [':' port-number]
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// component := alpha-numeric [separator alpha-numeric]*
// path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
@ -126,23 +126,56 @@ type Digested interface {
}
// Canonical reference is an object with a fully unique
// name including a name with hostname and digest
// name including a name with domain and digest
type Canonical interface {
Named
Digest() digest.Digest
}
// NamedRepository is a reference to a repository with a name.
// A NamedRepository has both domain and path components.
type NamedRepository interface {
Named
Domain() string
Path() string
}
// Domain returns the domain part of the Named reference
func Domain(named Named) string {
if r, ok := named.(NamedRepository); ok {
return r.Domain()
}
domain, _ := splitDomain(named.Name())
return domain
}
// Path returns the name without the domain part of the Named reference
func Path(named Named) (name string) {
if r, ok := named.(NamedRepository); ok {
return r.Path()
}
_, path := splitDomain(named.Name())
return path
}
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
}
return match[1], match[2]
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
// DEPRECATED: Use Domain or Path
func SplitHostname(named Named) (string, string) {
name := named.Name()
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
if r, ok := named.(NamedRepository); ok {
return r.Domain(), r.Path()
}
return match[1], match[2]
return splitDomain(named.Name())
}
// Parse parses s and returns a syntactically valid Reference.
@ -164,9 +197,20 @@ func Parse(s string) (Reference, error) {
return nil, ErrNameTooLong
}
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if nameMatch != nil && len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {
repo.domain = ""
repo.path = matches[1]
}
ref := reference{
name: matches[1],
tag: matches[2],
repository: repo,
tag: matches[2],
}
if matches[3] != "" {
var err error
@ -207,10 +251,15 @@ func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
if !anchoredNameRegexp.MatchString(name) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat
}
return repository(name), nil
return repository{
domain: match[1],
path: match[2],
}, nil
}
// WithTag combines the name from "name" and the tag from "tag" to form a
@ -219,16 +268,23 @@ func WithTag(name Named, tag string) (NamedTagged, error) {
if !anchoredTagRegexp.MatchString(tag) {
return nil, ErrTagInvalidFormat
}
var repo repository
if r, ok := name.(NamedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if canonical, ok := name.(Canonical); ok {
return reference{
name: name.Name(),
repository: repo,
tag: tag,
digest: canonical.Digest(),
}, nil
}
return taggedReference{
name: name.Name(),
tag: tag,
repository: repo,
tag: tag,
}, nil
}
@ -238,16 +294,23 @@ func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
if !anchoredDigestRegexp.MatchString(digest.String()) {
return nil, ErrDigestInvalidFormat
}
var repo repository
if r, ok := name.(NamedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if tagged, ok := name.(Tagged); ok {
return reference{
name: name.Name(),
repository: repo,
tag: tagged.Tag(),
digest: digest,
}, nil
}
return canonicalReference{
name: name.Name(),
digest: digest,
repository: repo,
digest: digest,
}, nil
}
@ -267,7 +330,7 @@ func TrimNamed(ref Named) Named {
}
func getBestReferenceType(ref reference) Reference {
if ref.name == "" {
if ref.repository.path == "" {
// Allow digest only references
if ref.digest != "" {
return digestReference(ref.digest)
@ -277,16 +340,16 @@ func getBestReferenceType(ref reference) Reference {
if ref.tag == "" {
if ref.digest != "" {
return canonicalReference{
name: ref.name,
digest: ref.digest,
repository: ref.repository,
digest: ref.digest,
}
}
return repository(ref.name)
return ref.repository
}
if ref.digest == "" {
return taggedReference{
name: ref.name,
tag: ref.tag,
repository: ref.repository,
tag: ref.tag,
}
}
@ -294,17 +357,13 @@ func getBestReferenceType(ref reference) Reference {
}
type reference struct {
name string
repository
tag string
digest digest.Digest
}
func (r reference) String() string {
return r.name + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Name() string {
return r.name
return r.Name() + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Tag() string {
@ -315,14 +374,28 @@ func (r reference) Digest() digest.Digest {
return r.digest
}
type repository string
type repository struct {
domain string
path string
}
func (r repository) String() string {
return string(r)
return r.Name()
}
func (r repository) Name() string {
return string(r)
if r.domain == "" {
return r.path
}
return r.domain + "/" + r.path
}
func (r repository) Domain() string {
return r.domain
}
func (r repository) Path() string {
return r.path
}
type digestReference digest.Digest
@ -336,16 +409,12 @@ func (d digestReference) Digest() digest.Digest {
}
type taggedReference struct {
name string
tag string
repository
tag string
}
func (t taggedReference) String() string {
return t.name + ":" + t.tag
}
func (t taggedReference) Name() string {
return t.name
return t.Name() + ":" + t.tag
}
func (t taggedReference) Tag() string {
@ -353,16 +422,12 @@ func (t taggedReference) Tag() string {
}
type canonicalReference struct {
name string
repository
digest digest.Digest
}
func (c canonicalReference) String() string {
return c.name + "@" + c.digest.String()
}
func (c canonicalReference) Name() string {
return c.name
return c.Name() + "@" + c.digest.String()
}
func (c canonicalReference) Digest() digest.Digest {

View file

@ -21,8 +21,8 @@ func TestReferenceParse(t *testing.T) {
err error
// repository is the string representation for the reference
repository string
// hostname is the hostname expected in the reference
hostname string
// domain is the domain expected in the reference
domain string
// tag is the tag for the reference
tag string
// digest is the digest for the reference (enforces digest reference)
@ -44,37 +44,37 @@ func TestReferenceParse(t *testing.T) {
},
{
input: "test.com/repo:tag",
hostname: "test.com",
domain: "test.com",
repository: "test.com/repo",
tag: "tag",
},
{
input: "test:5000/repo",
hostname: "test:5000",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "test:5000/repo:tag",
hostname: "test:5000",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
},
{
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
hostname: "test:5000",
domain: "test:5000",
repository: "test:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
hostname: "test:5000",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo",
hostname: "test:5000",
domain: "test:5000",
repository: "test:5000/repo",
},
{
@ -122,7 +122,7 @@ func TestReferenceParse(t *testing.T) {
},
{
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
hostname: "a",
domain: "a",
repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max",
},
@ -132,30 +132,30 @@ func TestReferenceParse(t *testing.T) {
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
hostname: "sub-dom1.foo.com",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
},
{
input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
hostname: "sub-dom1.foo.com",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
tag: "some-long-tag",
},
{
input: "b.gcr.io/test.example.com/my-app:test.example.com",
hostname: "b.gcr.io",
domain: "b.gcr.io",
repository: "b.gcr.io/test.example.com/my-app",
tag: "test.example.com",
},
{
input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
hostname: "xn--n3h.com",
domain: "xn--n3h.com",
repository: "xn--n3h.com/myimage",
tag: "xn--n3h.com",
},
{
input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
hostname: "xn--7o8h.com",
domain: "xn--7o8h.com",
repository: "xn--7o8h.com/myimage",
tag: "xn--7o8h.com",
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
@ -167,7 +167,7 @@ func TestReferenceParse(t *testing.T) {
},
{
input: "foo/foo_bar.com:8080",
hostname: "foo",
domain: "foo",
repository: "foo/foo_bar.com",
tag: "8080",
},
@ -198,11 +198,11 @@ func TestReferenceParse(t *testing.T) {
if named.Name() != testcase.repository {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
}
hostname, _ := SplitHostname(named)
if hostname != testcase.hostname {
failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname)
domain, _ := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
} else if testcase.repository != "" || testcase.hostname != "" {
} else if testcase.repository != "" || testcase.domain != "" {
failf("expected named type, got %T", repo)
}
@ -282,39 +282,39 @@ func TestWithNameFailure(t *testing.T) {
func TestSplitHostname(t *testing.T) {
testcases := []struct {
input string
hostname string
name string
input string
domain string
name string
}{
{
input: "test.com/foo",
hostname: "test.com",
name: "foo",
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
hostname: "",
name: "test_com/foo",
input: "test_com/foo",
domain: "",
name: "test_com/foo",
},
{
input: "test:8080/foo",
hostname: "test:8080",
name: "foo",
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test.com:8080/foo",
hostname: "test.com:8080",
name: "foo",
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
hostname: "test-com:8080",
name: "foo",
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
hostname: "xn--n3h.com:18080",
name: "foo",
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
}
for _, testcase := range testcases {
@ -327,9 +327,9 @@ func TestSplitHostname(t *testing.T) {
if err != nil {
failf("error parsing name: %s", err)
}
hostname, name := SplitHostname(named)
if hostname != testcase.hostname {
failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname)
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)

View file

@ -19,18 +19,18 @@ var (
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)))
// hostnameComponentRegexp restricts the registry hostname component of a
// repository name to start with a component as defined by hostnameRegexp
// domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by domainRegexp
// and followed by an optional port.
hostnameComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// hostnameRegexp defines the structure of potential hostname components
// domainRegexp defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names.
hostnameRegexp = expression(
hostnameComponentRegexp,
optional(repeated(literal(`.`), hostnameComponentRegexp)),
domainRegexp = expression(
domainComponentRegexp,
optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
@ -48,17 +48,17 @@ var (
anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the hostname and name part omitting
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = expression(
optional(hostnameRegexp, literal(`/`)),
optional(domainRegexp, literal(`/`)),
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the
// hostname and trailing components.
// domain and trailing components.
anchoredNameRegexp = anchored(
optional(capture(hostnameRegexp), literal(`/`)),
optional(capture(domainRegexp), literal(`/`)),
capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))))

View file

@ -33,7 +33,7 @@ func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) {
}
}
func TestHostRegexp(t *testing.T) {
func TestDomainRegexp(t *testing.T) {
hostcases := []regexpMatch{
{
input: "test.com",
@ -116,7 +116,7 @@ func TestHostRegexp(t *testing.T) {
match: true,
},
}
r := regexp.MustCompile(`^` + hostnameRegexp.String() + `$`)
r := regexp.MustCompile(`^` + domainRegexp.String() + `$`)
for i := range hostcases {
checkRegexp(t, r, hostcases[i])
}