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

View file

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

View file

@ -19,18 +19,18 @@ var (
alphaNumericRegexp, alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp))) optional(repeated(separatorRegexp, alphaNumericRegexp)))
// hostnameComponentRegexp restricts the registry hostname component of a // domainComponentRegexp restricts the registry domain component of a
// repository name to start with a component as defined by hostnameRegexp // repository name to start with a component as defined by domainRegexp
// and followed by an optional port. // 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 // 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 // allowed by DNS to ensure backwards compatibility with Docker image
// names. // names.
hostnameRegexp = expression( domainRegexp = expression(
hostnameComponentRegexp, domainComponentRegexp,
optional(repeated(literal(`.`), hostnameComponentRegexp)), optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`))) optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go. // TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
@ -48,17 +48,17 @@ var (
anchoredDigestRegexp = anchored(DigestRegexp) anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The // 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. // the separating forward slash from either.
NameRegexp = expression( NameRegexp = expression(
optional(hostnameRegexp, literal(`/`)), optional(domainRegexp, literal(`/`)),
nameComponentRegexp, nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))) optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the // anchoredNameRegexp is used to parse a name value, capturing the
// hostname and trailing components. // domain and trailing components.
anchoredNameRegexp = anchored( anchoredNameRegexp = anchored(
optional(capture(hostnameRegexp), literal(`/`)), optional(capture(domainRegexp), literal(`/`)),
capture(nameComponentRegexp, capture(nameComponentRegexp,
optional(repeated(literal(`/`), 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{ hostcases := []regexpMatch{
{ {
input: "test.com", input: "test.com",
@ -116,7 +116,7 @@ func TestHostRegexp(t *testing.T) {
match: true, match: true,
}, },
} }
r := regexp.MustCompile(`^` + hostnameRegexp.String() + `$`) r := regexp.MustCompile(`^` + domainRegexp.String() + `$`)
for i := range hostcases { for i := range hostcases {
checkRegexp(t, r, hostcases[i]) checkRegexp(t, r, hostcases[i])
} }