diff --git a/reference/reference.go b/reference/reference.go index 29b949aad..4723c3ecb 100644 --- a/reference/reference.go +++ b/reference/reference.go @@ -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 { diff --git a/reference/reference_test.go b/reference/reference_test.go index be708c4df..ef3d849fd 100644 --- a/reference/reference_test.go +++ b/reference/reference_test.go @@ -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) diff --git a/reference/regexp.go b/reference/regexp.go index 9a7d366bc..5ba4de853 100644 --- a/reference/regexp.go +++ b/reference/regexp.go @@ -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)))) diff --git a/reference/regexp_test.go b/reference/regexp_test.go index 2ec39377a..aef39bb12 100644 --- a/reference/regexp_test.go +++ b/reference/regexp_test.go @@ -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]) }