Merge pull request #1778 from dmcgowan/reference-with-split-hostname
Integrate docker reference changesmaster
						commit
						dbc336e1ff
					
				|  | @ -10,3 +10,21 @@ func IsNameOnly(ref Named) bool { | |||
| 	} | ||||
| 	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() | ||||
| } | ||||
|  |  | |||
|  | @ -1,9 +1,124 @@ | |||
| package reference | ||||
| 
 | ||||
| var ( | ||||
| 	defaultTag = "latest" | ||||
| import ( | ||||
| 	"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
 | ||||
| // a repo name.
 | ||||
| func EnsureTagged(ref Named) NamedTagged { | ||||
|  | @ -20,3 +135,33 @@ func EnsureTagged(ref Named) 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) | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -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                       := /[_.]|__|[-]*/
 | ||||
| //
 | ||||
|  | @ -19,6 +19,9 @@ | |||
| //	digest-algorithm-separator      := /[+.-_]/
 | ||||
| //	digest-algorithm-component      := /[A-Za-z][A-Za-z0-9]*/
 | ||||
| //	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 | ||||
| 
 | ||||
| import ( | ||||
|  | @ -126,23 +129,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 +200,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], | ||||
| 		NamedRepository: repo, | ||||
| 		tag:             matches[2], | ||||
| 	} | ||||
| 	if matches[3] != "" { | ||||
| 		var err error | ||||
|  | @ -207,10 +254,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 +271,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(), | ||||
| 			tag:    tag, | ||||
| 			digest: canonical.Digest(), | ||||
| 			NamedRepository: repo, | ||||
| 			tag:             tag, | ||||
| 			digest:          canonical.Digest(), | ||||
| 		}, nil | ||||
| 	} | ||||
| 	return taggedReference{ | ||||
| 		name: name.Name(), | ||||
| 		tag:  tag, | ||||
| 		NamedRepository: repo, | ||||
| 		tag:             tag, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  | @ -238,16 +297,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(), | ||||
| 			tag:    tagged.Tag(), | ||||
| 			digest: digest, | ||||
| 			NamedRepository: repo, | ||||
| 			tag:             tagged.Tag(), | ||||
| 			digest:          digest, | ||||
| 		}, nil | ||||
| 	} | ||||
| 	return canonicalReference{ | ||||
| 		name:   name.Name(), | ||||
| 		digest: digest, | ||||
| 		NamedRepository: repo, | ||||
| 		digest:          digest, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
|  | @ -263,11 +329,15 @@ func Match(pattern string, ref Reference) (bool, error) { | |||
| 
 | ||||
| // TrimNamed removes any tag or digest from the named reference.
 | ||||
| func TrimNamed(ref Named) Named { | ||||
| 	return repository(ref.Name()) | ||||
| 	domain, path := SplitHostname(ref) | ||||
| 	return repository{ | ||||
| 		domain: domain, | ||||
| 		path:   path, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getBestReferenceType(ref reference) Reference { | ||||
| 	if ref.name == "" { | ||||
| 	if ref.Name() == "" { | ||||
| 		// Allow digest only references
 | ||||
| 		if ref.digest != "" { | ||||
| 			return digestReference(ref.digest) | ||||
|  | @ -277,16 +347,16 @@ func getBestReferenceType(ref reference) Reference { | |||
| 	if ref.tag == "" { | ||||
| 		if ref.digest != "" { | ||||
| 			return canonicalReference{ | ||||
| 				name:   ref.name, | ||||
| 				digest: ref.digest, | ||||
| 				NamedRepository: ref.NamedRepository, | ||||
| 				digest:          ref.digest, | ||||
| 			} | ||||
| 		} | ||||
| 		return repository(ref.name) | ||||
| 		return ref.NamedRepository | ||||
| 	} | ||||
| 	if ref.digest == "" { | ||||
| 		return taggedReference{ | ||||
| 			name: ref.name, | ||||
| 			tag:  ref.tag, | ||||
| 			NamedRepository: ref.NamedRepository, | ||||
| 			tag:             ref.tag, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | @ -294,17 +364,13 @@ func getBestReferenceType(ref reference) Reference { | |||
| } | ||||
| 
 | ||||
| type reference struct { | ||||
| 	name   string | ||||
| 	NamedRepository | ||||
| 	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 +381,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 +416,12 @@ func (d digestReference) Digest() digest.Digest { | |||
| } | ||||
| 
 | ||||
| type taggedReference struct { | ||||
| 	name string | ||||
| 	tag  string | ||||
| 	NamedRepository | ||||
| 	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 +429,12 @@ func (t taggedReference) Tag() string { | |||
| } | ||||
| 
 | ||||
| type canonicalReference struct { | ||||
| 	name   string | ||||
| 	NamedRepository | ||||
| 	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 { | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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)))) | ||||
| 
 | ||||
|  | @ -68,6 +68,25 @@ var ( | |||
| 	ReferenceRegexp = anchored(capture(NameRegexp), | ||||
| 		optional(literal(":"), capture(TagRegexp)), | ||||
| 		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.
 | ||||
|  |  | |||
|  | @ -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]) | ||||
| 	} | ||||
|  | @ -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]) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue