Merge pull request #1778 from dmcgowan/reference-with-split-hostname

Integrate docker reference changes
This commit is contained in:
Aaron Lehmann 2017-01-09 15:17:10 -08:00 committed by GitHub
commit dbc336e1ff
7 changed files with 865 additions and 111 deletions

View file

@ -10,3 +10,21 @@ func IsNameOnly(ref Named) bool {
} }
return true return true
} }
// FamiliarName returns the familiar name string
// for the given named, familiarizing if needed.
func FamiliarName(ref Named) string {
if nn, ok := ref.(NormalizedNamed); ok {
return nn.Familiar().Name()
}
return ref.Name()
}
// FamiliarString returns the familiar string representation
// for the given reference, familiarizing if needed.
func FamiliarString(ref Reference) string {
if nn, ok := ref.(NormalizedNamed); ok {
return nn.Familiar().String()
}
return ref.String()
}

View file

@ -1,9 +1,124 @@
package reference package reference
var ( import (
defaultTag = "latest" "errors"
"fmt"
"strings"
"github.com/docker/distribution/digest"
) )
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
defaultRepoPrefix = "library/"
defaultTag = "latest"
)
// NormalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type NormalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (NormalizedNamed, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, errors.New("invalid reference format: repository name must be lowercase")
}
ref, err := Parse(domain + "/" + remainder)
if err != nil {
return nil, err
}
named, isNamed := ref.(NormalizedNamed)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// splitDockerDomain splits a repository name to domain and remotename string.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = defaultRepoPrefix + remainder
}
return
}
// familiarizeName returns a shortened version of the name familiar
// to to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named NamedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
repo.path = strings.TrimPrefix(repo.path, defaultRepoPrefix)
}
return repo
}
func (r reference) Familiar() Named {
return reference{
NamedRepository: familiarizeName(r.NamedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
NamedRepository: familiarizeName(t.NamedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
NamedRepository: familiarizeName(c.NamedRepository),
digest: c.digest,
}
}
// EnsureTagged adds the default tag "latest" to a reference if it only has // EnsureTagged adds the default tag "latest" to a reference if it only has
// a repo name. // a repo name.
func EnsureTagged(ref Named) NamedTagged { func EnsureTagged(ref Named) NamedTagged {
@ -20,3 +135,33 @@ func EnsureTagged(ref Named) NamedTagged {
} }
return namedTagged return namedTagged
} }
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.ParseDigest(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}
// ParseAnyReferenceWithSet parses a reference string as a possible short
// identifier to be matched in a digest set, a full digest, or familiar name.
func ParseAnyReferenceWithSet(ref string, ds *digest.Set) (Reference, error) {
if ok := anchoredShortIdentifierRegexp.MatchString(ref); ok {
dgst, err := ds.Lookup(ref)
if err == nil {
return digestReference(dgst), nil
}
} else {
if dgst, err := digest.ParseDigest(ref); err == nil {
return digestReference(dgst), nil
}
}
return ParseNormalizedNamed(ref)
}

436
reference/normalize_test.go Normal file
View file

@ -0,0 +1,436 @@
package reference
import (
"testing"
"github.com/docker/distribution/digest"
)
func TestValidateReferenceName(t *testing.T) {
validRepoNames := []string{
"docker/docker",
"library/debian",
"debian",
"docker.io/docker/docker",
"docker.io/library/debian",
"docker.io/debian",
"index.docker.io/docker/docker",
"index.docker.io/library/debian",
"index.docker.io/debian",
"127.0.0.1:5000/docker/docker",
"127.0.0.1:5000/library/debian",
"127.0.0.1:5000/debian",
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// This test case was moved from invalid to valid since it is valid input
// when specified with a hostname, it removes the ambiguity from about
// whether the value is an identifier or repository name
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
invalidRepoNames := []string{
"https://github.com/docker/docker",
"docker/Docker",
"-docker",
"-docker/docker",
"-docker.io/docker/docker",
"docker///docker",
"docker.io/docker/Docker",
"docker.io/docker///docker",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
for _, name := range invalidRepoNames {
_, err := ParseNormalizedNamed(name)
if err == nil {
t.Fatalf("Expected invalid repo name for %q", name)
}
}
for _, name := range validRepoNames {
_, err := ParseNormalizedNamed(name)
if err != nil {
t.Fatalf("Error parsing repo name %s, got: %q", name, err)
}
}
}
func TestValidateRemoteName(t *testing.T) {
validRepositoryNames := []string{
// Sanity check.
"docker/docker",
// Allow 64-character non-hexadecimal names (hexadecimal names are forbidden).
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// Allow embedded hyphens.
"docker-rules/docker",
// Allow multiple hyphens as well.
"docker---rules/docker",
//Username doc and image name docker being tested.
"doc/docker",
// single character names are now allowed.
"d/docker",
"jess/t",
// Consecutive underscores.
"dock__er/docker",
}
for _, repositoryName := range validRepositoryNames {
_, err := ParseNormalizedNamed(repositoryName)
if err != nil {
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
}
}
invalidRepositoryNames := []string{
// Disallow capital letters.
"docker/Docker",
// Only allow one slash.
"docker///docker",
// Disallow 64-character hexadecimal.
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// Disallow leading and trailing hyphens in namespace.
"-docker/docker",
"docker-/docker",
"-docker-/docker",
// Don't allow underscores everywhere (as opposed to hyphens).
"____/____",
"_docker/_docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",
// No repository.
"docker/",
//namespace too long
"this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker",
}
for _, repositoryName := range invalidRepositoryNames {
if _, err := ParseNormalizedNamed(repositoryName); err == nil {
t.Errorf("Repository name should be invalid: %v", repositoryName)
}
}
}
func TestParseRepositoryInfo(t *testing.T) {
type tcase struct {
RemoteName, FamiliarName, FullName, AmbiguousName, Domain string
}
tcases := []tcase{
{
RemoteName: "fooo/bar",
FamiliarName: "fooo/bar",
FullName: "docker.io/fooo/bar",
AmbiguousName: "index.docker.io/fooo/bar",
Domain: "docker.io",
},
{
RemoteName: "library/ubuntu",
FamiliarName: "ubuntu",
FullName: "docker.io/library/ubuntu",
AmbiguousName: "library/ubuntu",
Domain: "docker.io",
},
{
RemoteName: "nonlibrary/ubuntu",
FamiliarName: "nonlibrary/ubuntu",
FullName: "docker.io/nonlibrary/ubuntu",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "other/library",
FamiliarName: "other/library",
FullName: "docker.io/other/library",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "private/moonbase",
FamiliarName: "127.0.0.1:8000/private/moonbase",
FullName: "127.0.0.1:8000/private/moonbase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "privatebase",
FamiliarName: "127.0.0.1:8000/privatebase",
FullName: "127.0.0.1:8000/privatebase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com/private/moonbase",
FullName: "example.com/private/moonbase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "privatebase",
FamiliarName: "example.com/privatebase",
FullName: "example.com/privatebase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com:8000/private/moonbase",
FullName: "example.com:8000/private/moonbase",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "privatebasee",
FamiliarName: "example.com:8000/privatebasee",
FullName: "example.com:8000/privatebasee",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "library/ubuntu-12.04-base",
FamiliarName: "ubuntu-12.04-base",
FullName: "docker.io/library/ubuntu-12.04-base",
AmbiguousName: "index.docker.io/library/ubuntu-12.04-base",
Domain: "docker.io",
},
}
for _, tcase := range tcases {
refStrings := []string{tcase.FamiliarName, tcase.FullName}
if tcase.AmbiguousName != "" {
refStrings = append(refStrings, tcase.AmbiguousName)
}
var refs []NormalizedNamed
for _, r := range refStrings {
named, err := ParseNormalizedNamed(r)
if err != nil {
t.Fatal(err)
}
refs = append(refs, named)
}
for _, r := range refs {
if expected, actual := tcase.FamiliarName, r.Familiar().Name(); expected != actual {
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.FullName, r.String(); expected != actual {
t.Fatalf("Invalid canonical reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.Domain, Domain(r); expected != actual {
t.Fatalf("Invalid domain for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.RemoteName, Path(r); expected != actual {
t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual)
}
}
}
}
func TestParseReferenceWithTagAndDigest(t *testing.T) {
shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"
nref, err := ParseNormalizedNamed(shortRef)
if err != nil {
t.Fatal(err)
}
if expected, actual := "docker.io/library/"+shortRef, nref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", nref, expected, actual)
}
ref := nref.Familiar()
if _, isTagged := ref.(NamedTagged); !isTagged {
t.Fatalf("Reference from %q should support tag", ref)
}
if _, isCanonical := ref.(Canonical); !isCanonical {
t.Fatalf("Reference from %q should support digest", ref)
}
if expected, actual := shortRef, ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
}
func TestInvalidReferenceComponents(t *testing.T) {
if _, err := ParseNormalizedNamed("-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid name")
}
ref, err := ParseNormalizedNamed("busybox")
if err != nil {
t.Fatal(err)
}
if _, err := WithTag(ref, "-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid tag")
}
if _, err := WithDigest(ref, digest.Digest("foo")); err == nil {
t.Fatal("Expected WithDigest to detect invalid digest")
}
}
func equalReference(r1, r2 Reference) bool {
switch v1 := r1.(type) {
case digestReference:
if v2, ok := r2.(digestReference); ok {
return v1 == v2
}
case repository:
if v2, ok := r2.(repository); ok {
return v1 == v2
}
case taggedReference:
if v2, ok := r2.(taggedReference); ok {
return v1 == v2
}
case canonicalReference:
if v2, ok := r2.(canonicalReference); ok {
return v1 == v2
}
case reference:
if v2, ok := r2.(reference); ok {
return v1 == v2
}
}
return false
}
func TestParseAnyReference(t *testing.T) {
tcases := []struct {
Reference string
Equivalent string
Expected Reference
Digests []digest.Digest
}{
{
Reference: "redis",
Equivalent: "docker.io/library/redis",
},
{
Reference: "redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "docker.io/library/redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dmcgowan/myapp",
Equivalent: "docker.io/dmcgowan/myapp",
},
{
Reference: "dmcgowan/myapp:latest",
Equivalent: "docker.io/dmcgowan/myapp:latest",
},
{
Reference: "docker.io/mcgowan/myapp:latest",
Equivalent: "docker.io/mcgowan/myapp:latest",
},
{
Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
{
Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Digests: []digest.Digest{
digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Digests: []digest.Digest{
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Digests: []digest.Digest{
digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1",
Equivalent: "docker.io/library/dbcc1",
Digests: []digest.Digest{
digest.Digest("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
{
Reference: "dbcc1c",
Equivalent: "docker.io/library/dbcc1c",
Digests: []digest.Digest{
digest.Digest("sha256:abcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
},
},
}
for _, tcase := range tcases {
var ref Reference
var err error
if len(tcase.Digests) == 0 {
ref, err = ParseAnyReference(tcase.Reference)
} else {
ds := digest.NewSet()
for _, dgst := range tcase.Digests {
if err := ds.Add(dgst); err != nil {
t.Fatalf("Error adding digest %s: %v", dgst.String(), err)
}
}
ref, err = ParseAnyReferenceWithSet(tcase.Reference, ds)
}
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Reference, err)
}
expected := tcase.Expected
if expected == nil {
expected, err = Parse(tcase.Equivalent)
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Equivalent, err)
}
}
if !equalReference(ref, expected) {
t.Errorf("Unexpected reference %#v, expected %#v", ref, expected)
}
}
}

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 := /[_.]|__|[-]*/
// //
@ -19,6 +19,9 @@
// digest-algorithm-separator := /[+.-_]/ // digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/
package reference package reference
import ( import (
@ -126,23 +129,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,9 +200,20 @@ 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], NamedRepository: repo,
tag: matches[2], tag: matches[2],
} }
if matches[3] != "" { if matches[3] != "" {
var err error var err error
@ -207,10 +254,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,16 +271,23 @@ 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(), NamedRepository: repo,
tag: tag, tag: tag,
digest: canonical.Digest(), digest: canonical.Digest(),
}, nil }, nil
} }
return taggedReference{ return taggedReference{
name: name.Name(), NamedRepository: repo,
tag: tag, tag: tag,
}, nil }, nil
} }
@ -238,16 +297,23 @@ 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(), NamedRepository: repo,
tag: tagged.Tag(), tag: tagged.Tag(),
digest: digest, digest: digest,
}, nil }, nil
} }
return canonicalReference{ return canonicalReference{
name: name.Name(), NamedRepository: repo,
digest: digest, digest: digest,
}, nil }, nil
} }
@ -263,11 +329,15 @@ func Match(pattern string, ref Reference) (bool, error) {
// TrimNamed removes any tag or digest from the named reference. // TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named { func TrimNamed(ref Named) Named {
return repository(ref.Name()) domain, path := SplitHostname(ref)
return repository{
domain: domain,
path: path,
}
} }
func getBestReferenceType(ref reference) Reference { func getBestReferenceType(ref reference) Reference {
if ref.name == "" { if ref.Name() == "" {
// Allow digest only references // Allow digest only references
if ref.digest != "" { if ref.digest != "" {
return digestReference(ref.digest) return digestReference(ref.digest)
@ -277,16 +347,16 @@ func getBestReferenceType(ref reference) Reference {
if ref.tag == "" { if ref.tag == "" {
if ref.digest != "" { if ref.digest != "" {
return canonicalReference{ return canonicalReference{
name: ref.name, NamedRepository: ref.NamedRepository,
digest: ref.digest, digest: ref.digest,
} }
} }
return repository(ref.name) return ref.NamedRepository
} }
if ref.digest == "" { if ref.digest == "" {
return taggedReference{ return taggedReference{
name: ref.name, NamedRepository: ref.NamedRepository,
tag: ref.tag, tag: ref.tag,
} }
} }
@ -294,17 +364,13 @@ func getBestReferenceType(ref reference) Reference {
} }
type reference struct { type reference struct {
name string NamedRepository
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 +381,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 +416,12 @@ func (d digestReference) Digest() digest.Digest {
} }
type taggedReference struct { type taggedReference struct {
name string NamedRepository
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 +429,12 @@ func (t taggedReference) Tag() string {
} }
type canonicalReference struct { type canonicalReference struct {
name string NamedRepository
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)
} }
@ -282,39 +282,39 @@ 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",
}, },
} }
for _, testcase := range testcases { for _, testcase := range testcases {
@ -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))))
@ -68,6 +68,25 @@ var (
ReferenceRegexp = anchored(capture(NameRegexp), ReferenceRegexp = anchored(capture(NameRegexp),
optional(literal(":"), capture(TagRegexp)), optional(literal(":"), capture(TagRegexp)),
optional(literal("@"), capture(DigestRegexp))) optional(literal("@"), capture(DigestRegexp)))
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
IdentifierRegexp = match(`([a-f0-9]{64})`)
// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
ShortIdentifierRegexp = match(`([a-f0-9]{6,64})`)
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = anchored(IdentifierRegexp)
// anchoredShortIdentifierRegexp is used to check if a value
// is a possible identifier prefix, anchored at start and end
// of string.
anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp)
) )
// match compiles the string to a regular expression. // match compiles the string to a regular expression.

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])
} }
@ -487,3 +487,67 @@ func TestReferenceRegexp(t *testing.T) {
} }
} }
func TestIdentifierRegexp(t *testing.T) {
fullCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: false,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
}
shortCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: true,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
{
input: "da304",
match: false,
},
{
input: "da304e",
match: true,
},
}
for i := range fullCases {
checkRegexp(t, anchoredIdentifierRegexp, fullCases[i])
}
for i := range shortCases {
checkRegexp(t, anchoredShortIdentifierRegexp, shortCases[i])
}
}