Vendoring with Glide and delete function handler

This commit is contained in:
Alex Ellis
2017-04-26 19:13:42 +01:00
parent 1eaf13c6c8
commit 78af82021f
7137 changed files with 1688302 additions and 19 deletions

View File

@ -0,0 +1,42 @@
package reference
import "path"
// IsNameOnly returns true if reference only contains a repo name.
func IsNameOnly(ref Named) bool {
if _, ok := ref.(NamedTagged); ok {
return false
}
if _, ok := ref.(Canonical); ok {
return false
}
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()
}
// FamiliarMatch reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
func FamiliarMatch(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, FamiliarString(ref))
if namedRef, isNamed := ref.(Named); isNamed && !matched {
matched, _ = path.Match(pattern, FamiliarName(namedRef))
}
return matched, err
}

View File

@ -0,0 +1,170 @@
package reference
import (
"errors"
"fmt"
"strings"
"github.com/docker/distribution/digestset"
"github.com/opencontainers/go-digest"
)
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
officialRepoName = "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) (Named, 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.(Named)
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 = officialRepoName + "/" + 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 = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
repo.path = split[1]
}
}
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,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// 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.Parse(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 *digestset.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.Parse(ref); err == nil {
return digestReference(dgst), nil
}
}
return ParseNormalizedNamed(ref)
}

View File

@ -0,0 +1,625 @@
package reference
import (
"strconv"
"testing"
"github.com/docker/distribution/digestset"
"github.com/opencontainers/go-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",
},
{
RemoteName: "library/foo",
FamiliarName: "foo",
FullName: "docker.io/library/foo",
AmbiguousName: "docker.io/foo",
Domain: "docker.io",
},
{
RemoteName: "library/foo/bar",
FamiliarName: "library/foo/bar",
FullName: "docker.io/library/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "store/foo/bar",
FamiliarName: "store/foo/bar",
FullName: "docker.io/store/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
}
for _, tcase := range tcases {
refStrings := []string{tcase.FamiliarName, tcase.FullName}
if tcase.AmbiguousName != "" {
refStrings = append(refStrings, tcase.AmbiguousName)
}
var refs []Named
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, FamiliarName(r); 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"
ref, err := ParseNormalizedNamed(shortRef)
if err != nil {
t.Fatal(err)
}
if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
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, FamiliarString(ref); 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"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "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"),
Equivalent: "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 := digestset.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)
}
if ref.String() != tcase.Equivalent {
t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tcase.Equivalent)
}
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)
}
}
}
func TestNormalizedSplitHostname(t *testing.T) {
testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "docker.io",
name: "test_com/foo",
},
{
input: "docker/migrator",
domain: "docker.io",
name: "docker/migrator",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "xn--n3h.com/foo",
domain: "xn--n3h.com",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
{
input: "docker.io/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo/bar",
domain: "docker.io",
name: "library/foo/bar",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := ParseNormalizedNamed(testcase.input)
if err != nil {
failf("error parsing name: %s", err)
}
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)
}
}
}
func TestMatchError(t *testing.T) {
named, err := ParseAnyReference("foo")
if err != nil {
t.Fatal(err)
}
_, err = FamiliarMatch("[-x]", named)
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestMatch(t *testing.T) {
matchCases := []struct {
reference string
pattern string
expected bool
}{
{
reference: "foo",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/any/bat",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/a/bar",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/b/baz",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/*/baz:tag",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/c/baz:tag",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "*/foo/c/baz",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "example.com/foo/c/baz",
expected: true,
},
}
for _, c := range matchCases {
named, err := ParseAnyReference(c.reference)
if err != nil {
t.Fatal(err)
}
actual, err := FamiliarMatch(c.pattern, named)
if err != nil {
t.Fatal(err)
}
if actual != c.expected {
t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual)
}
}
}

View File

@ -0,0 +1,433 @@
// Package reference provides a general type to represent any way of referencing images within the registry.
// Its main purpose is to abstract tags and digests (content-addressable hash).
//
// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// 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]+/
// path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
// tag := /[\w][\w.-]{0,127}/
//
// digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
// 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 (
"errors"
"fmt"
"strings"
"github.com/opencontainers/go-digest"
)
const (
// NameTotalLengthMax is the maximum total number of characters in a repository name.
NameTotalLengthMax = 255
)
var (
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
ErrReferenceInvalidFormat = errors.New("invalid reference format")
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
ErrTagInvalidFormat = errors.New("invalid tag format")
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
ErrDigestInvalidFormat = errors.New("invalid digest format")
// ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
ErrNameContainsUppercase = errors.New("repository name must be lowercase")
// ErrNameEmpty is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component")
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
// ErrNameNotCanonical is returned when a name is not canonical.
ErrNameNotCanonical = errors.New("repository name must be canonical")
)
// Reference is an opaque object reference identifier that may include
// modifiers such as a hostname, name, tag, and digest.
type Reference interface {
// String returns the full reference
String() string
}
// Field provides a wrapper type for resolving correct reference types when
// working with encoding.
type Field struct {
reference Reference
}
// AsField wraps a reference in a Field for encoding.
func AsField(reference Reference) Field {
return Field{reference}
}
// Reference unwraps the reference type from the field to
// return the Reference object. This object should be
// of the appropriate type to further check for different
// reference types.
func (f Field) Reference() Reference {
return f.reference
}
// MarshalText serializes the field to byte text which
// is the string of the reference.
func (f Field) MarshalText() (p []byte, err error) {
return []byte(f.reference.String()), nil
}
// UnmarshalText parses text bytes by invoking the
// reference parser to ensure the appropriately
// typed reference object is wrapped by field.
func (f *Field) UnmarshalText(p []byte) error {
r, err := Parse(string(p))
if err != nil {
return err
}
f.reference = r
return nil
}
// Named is an object with a full name
type Named interface {
Reference
Name() string
}
// Tagged is an object which has a tag
type Tagged interface {
Reference
Tag() string
}
// NamedTagged is an object including a name and tag.
type NamedTagged interface {
Named
Tag() string
}
// Digested is an object which has a digest
// in which it can be referenced by
type Digested interface {
Reference
Digest() digest.Digest
}
// Canonical reference is an object with a fully unique
// 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) {
if r, ok := named.(namedRepository); ok {
return r.Domain(), r.Path()
}
return splitDomain(named.Name())
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: Parse will not handle short digests.
func Parse(s string) (Reference, error) {
matches := ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
if s == "" {
return nil, ErrNameEmpty
}
if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
return nil, ErrNameContainsUppercase
}
return nil, ErrReferenceInvalidFormat
}
if len(matches[1]) > NameTotalLengthMax {
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{
namedRepository: repo,
tag: matches[2],
}
if matches[3] != "" {
var err error
ref.digest, err = digest.Parse(matches[3])
if err != nil {
return nil, err
}
}
r := getBestReferenceType(ref)
if r == nil {
return nil, ErrNameEmpty
}
return r, nil
}
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name and be in the canonical
// form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) {
named, err := ParseNormalizedNamed(s)
if err != nil {
return nil, err
}
if named.String() != s {
return nil, ErrNameNotCanonical
}
return named, nil
}
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat
}
return repository{
domain: match[1],
path: match[2],
}, nil
}
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
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{
namedRepository: repo,
tag: tag,
digest: canonical.Digest(),
}, nil
}
return taggedReference{
namedRepository: repo,
tag: tag,
}, nil
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
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{
namedRepository: repo,
tag: tagged.Tag(),
digest: digest,
}, nil
}
return canonicalReference{
namedRepository: repo,
digest: digest,
}, nil
}
// TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named {
domain, path := SplitHostname(ref)
return repository{
domain: domain,
path: path,
}
}
func getBestReferenceType(ref reference) Reference {
if ref.Name() == "" {
// Allow digest only references
if ref.digest != "" {
return digestReference(ref.digest)
}
return nil
}
if ref.tag == "" {
if ref.digest != "" {
return canonicalReference{
namedRepository: ref.namedRepository,
digest: ref.digest,
}
}
return ref.namedRepository
}
if ref.digest == "" {
return taggedReference{
namedRepository: ref.namedRepository,
tag: ref.tag,
}
}
return ref
}
type reference struct {
namedRepository
tag string
digest digest.Digest
}
func (r reference) String() string {
return r.Name() + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Tag() string {
return r.tag
}
func (r reference) Digest() digest.Digest {
return r.digest
}
type repository struct {
domain string
path string
}
func (r repository) String() string {
return r.Name()
}
func (r repository) Name() string {
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
func (d digestReference) String() string {
return digest.Digest(d).String()
}
func (d digestReference) Digest() digest.Digest {
return digest.Digest(d)
}
type taggedReference struct {
namedRepository
tag string
}
func (t taggedReference) String() string {
return t.Name() + ":" + t.tag
}
func (t taggedReference) Tag() string {
return t.tag
}
type canonicalReference struct {
namedRepository
digest digest.Digest
}
func (c canonicalReference) String() string {
return c.Name() + "@" + c.digest.String()
}
func (c canonicalReference) Digest() digest.Digest {
return c.digest
}

View File

@ -0,0 +1,659 @@
package reference
import (
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/json"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
)
func TestReferenceParse(t *testing.T) {
// referenceTestcases is a unified set of testcases for
// testing the parsing of references
referenceTestcases := []struct {
// input is the repository name or name component testcase
input string
// err is the error expected from Parse, or nil
err error
// repository is the string representation for the reference
repository 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)
digest string
}{
{
input: "test_com",
repository: "test_com",
},
{
input: "test.com:tag",
repository: "test.com",
tag: "tag",
},
{
input: "test.com:5000",
repository: "test.com",
tag: "5000",
},
{
input: "test.com/repo:tag",
domain: "test.com",
repository: "test.com/repo",
tag: "tag",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "test:5000/repo:tag",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
},
{
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "repo@sha256:ffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestInvalidLength,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestUnsupported,
},
{
input: "Uppercase:tag",
err: ErrNameContainsUppercase,
},
// FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes.
// See https://github.com/docker/distribution/pull/1778, and https://github.com/docker/docker/pull/20175
//{
// input: "Uppercase/lowercase:tag",
// err: ErrNameContainsUppercase,
//},
{
input: "test:5000/Uppercase/lowercase:tag",
err: ErrNameContainsUppercase,
},
{
input: "lowercase:Uppercase",
repository: "lowercase",
tag: "Uppercase",
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
domain: "a",
repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max",
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
},
{
input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
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",
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
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
domain: "xn--7o8h.com",
repository: "xn--7o8h.com/myimage",
tag: "xn--7o8h.com",
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "foo_bar.com:8080",
repository: "foo_bar.com",
tag: "8080",
},
{
input: "foo/foo_bar.com:8080",
domain: "foo",
repository: "foo/foo_bar.com",
tag: "8080",
},
}
for _, testcase := range referenceTestcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
repo, err := Parse(testcase.input)
if testcase.err != nil {
if err == nil {
failf("missing expected error: %v", testcase.err)
} else if testcase.err != err {
failf("mismatched error: got %v, expected %v", err, testcase.err)
}
continue
} else if err != nil {
failf("unexpected parse error: %v", err)
continue
}
if repo.String() != testcase.input {
failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input)
}
if named, ok := repo.(Named); ok {
if named.Name() != testcase.repository {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
}
domain, _ := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
} else if testcase.repository != "" || testcase.domain != "" {
failf("expected named type, got %T", repo)
}
tagged, ok := repo.(Tagged)
if testcase.tag != "" {
if ok {
if tagged.Tag() != testcase.tag {
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
}
} else {
failf("expected tagged type, got %T", repo)
}
} else if ok {
failf("unexpected tagged type")
}
digested, ok := repo.(Digested)
if testcase.digest != "" {
if ok {
if digested.Digest().String() != testcase.digest {
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
}
} else {
failf("expected digested type, got %T", repo)
}
} else if ok {
failf("unexpected digested type")
}
}
}
// TestWithNameFailure tests cases where WithName should fail. Cases where it
// should succeed are covered by TestSplitHostname, below.
func TestWithNameFailure(t *testing.T) {
testcases := []struct {
input string
err error
}{
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
_, err := WithName(testcase.input)
if err == nil {
failf("no error parsing name. expected: %s", testcase.err)
}
}
}
func TestSplitHostname(t *testing.T) {
testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "",
name: "test_com/foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.input)
if err != nil {
failf("error parsing name: %s", err)
}
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)
}
}
}
type serializationType struct {
Description string
Field Field
}
func TestSerialization(t *testing.T) {
testcases := []struct {
description string
input string
name string
tag string
digest string
err error
}{
{
description: "empty value",
err: ErrNameEmpty,
},
{
description: "just a name",
input: "example.com:8000/named",
name: "example.com:8000/named",
},
{
description: "name with a tag",
input: "example.com:8000/named:tagged",
name: "example.com:8000/named",
tag: "tagged",
},
{
description: "name with digest",
input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112",
name: "other.com/named",
digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
m := map[string]string{
"Description": testcase.description,
"Field": testcase.input,
}
b, err := json.Marshal(m)
if err != nil {
failf("error marshalling: %v", err)
}
t := serializationType{}
if err := json.Unmarshal(b, &t); err != nil {
if testcase.err == nil {
failf("error unmarshalling: %v", err)
}
if err != testcase.err {
failf("wrong error, expected %v, got %v", testcase.err, err)
}
continue
} else if testcase.err != nil {
failf("expected error unmarshalling: %v", testcase.err)
}
if t.Description != testcase.description {
failf("wrong description, expected %q, got %q", testcase.description, t.Description)
}
ref := t.Field.Reference()
if named, ok := ref.(Named); ok {
if named.Name() != testcase.name {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name)
}
} else if testcase.name != "" {
failf("expected named type, got %T", ref)
}
tagged, ok := ref.(Tagged)
if testcase.tag != "" {
if ok {
if tagged.Tag() != testcase.tag {
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
}
} else {
failf("expected tagged type, got %T", ref)
}
} else if ok {
failf("unexpected tagged type")
}
digested, ok := ref.(Digested)
if testcase.digest != "" {
if ok {
if digested.Digest().String() != testcase.digest {
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
}
} else {
failf("expected digested type, got %T", ref)
}
} else if ok {
failf("unexpected digested type")
}
t = serializationType{
Description: testcase.description,
Field: AsField(ref),
}
b2, err := json.Marshal(t)
if err != nil {
failf("error marshing serialization type: %v", err)
}
if string(b) != string(b2) {
failf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
}
// Ensure t.Field is not implementing "Reference" directly, getting
// around the Reference type system
var fieldInterface interface{} = t.Field
if _, ok := fieldInterface.(Reference); ok {
failf("field should not implement Reference interface")
}
}
}
func TestWithTag(t *testing.T) {
testcases := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
tag: "tag",
combined: "test.com/foo:tag",
},
{
name: "foo",
tag: "tag2",
combined: "foo:tag2",
},
{
name: "test.com:8000/foo",
tag: "tag4",
combined: "test.com:8000/foo:tag4",
},
{
name: "test.com:8000/foo",
tag: "TAG5",
combined: "test.com:8000/foo:TAG5",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "TAG5",
combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.name)
if err != nil {
failf("error parsing name: %s", err)
}
if testcase.digest != "" {
canonical, err := WithDigest(named, testcase.digest)
if err != nil {
failf("error adding digest")
}
named = canonical
}
tagged, err := WithTag(named, testcase.tag)
if err != nil {
failf("WithTag failed: %s", err)
}
if tagged.String() != testcase.combined {
failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined)
}
}
}
func TestWithDigest(t *testing.T) {
testcases := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "latest",
combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.name)
if err != nil {
failf("error parsing name: %s", err)
}
if testcase.tag != "" {
tagged, err := WithTag(named, testcase.tag)
if err != nil {
failf("error adding tag")
}
named = tagged
}
digested, err := WithDigest(named, testcase.digest)
if err != nil {
failf("WithDigest failed: %s", err)
}
if digested.String() != testcase.combined {
failf("unexpected: got %q, expected %q", digested.String(), testcase.combined)
}
}
}
func TestParseNamed(t *testing.T) {
testcases := []struct {
input string
domain string
name string
err error
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test_com/foo",
err: ErrNameNotCanonical,
},
{
input: "test.com",
err: ErrNameNotCanonical,
},
{
input: "foo",
err: ErrNameNotCanonical,
},
{
input: "library/foo",
err: ErrNameNotCanonical,
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
// Ambiguous case, parser will add "library/" to foo
{
input: "docker.io/foo",
err: ErrNameNotCanonical,
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := ParseNamed(testcase.input)
if err != nil && testcase.err == nil {
failf("error parsing name: %s", err)
continue
} else if err == nil && testcase.err != nil {
failf("parsing succeded: expected error %v", testcase.err)
continue
} else if err != testcase.err {
failf("unexpected error %v, expected %v", err, testcase.err)
continue
} else if err != nil {
continue
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
}
}

View File

@ -0,0 +1,143 @@
package reference
import "regexp"
var (
// alphaNumericRegexp defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphaNumericRegexp = match(`[a-z0-9]+`)
// separatorRegexp defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// dashes.
separatorRegexp = match(`(?:[._]|__|[-]*)`)
// nameComponentRegexp restricts registry path component names to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
nameComponentRegexp = expression(
alphaNumericRegexp,
optional(repeated(separatorRegexp, alphaNumericRegexp)))
// 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.
domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`)
// 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.
DomainRegexp = expression(
domainComponentRegexp,
optional(repeated(literal(`.`), domainComponentRegexp)),
optional(literal(`:`), match(`[0-9]+`)))
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = match(`[\w][\w.-]{0,127}`)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = anchored(TagRegexp)
// DigestRegexp matches valid digests.
DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = anchored(DigestRegexp)
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = expression(
optional(DomainRegexp, literal(`/`)),
nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp)))
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = anchored(
optional(capture(DomainRegexp), literal(`/`)),
capture(nameComponentRegexp,
optional(repeated(literal(`/`), nameComponentRegexp))))
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
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.
var match = regexp.MustCompile
// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
func literal(s string) *regexp.Regexp {
re := match(regexp.QuoteMeta(s))
if _, complete := re.LiteralPrefix(); !complete {
panic("must be a literal")
}
return re
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...*regexp.Regexp) *regexp.Regexp {
var s string
for _, re := range res {
s += re.String()
}
return match(s)
}
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `?`)
}
// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
func repeated(res ...*regexp.Regexp) *regexp.Regexp {
return match(group(expression(res...)).String() + `+`)
}
// group wraps the regexp in a non-capturing group.
func group(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(?:` + expression(res...).String() + `)`)
}
// capture wraps the expression in a capturing group.
func capture(res ...*regexp.Regexp) *regexp.Regexp {
return match(`(` + expression(res...).String() + `)`)
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...*regexp.Regexp) *regexp.Regexp {
return match(`^` + expression(res...).String() + `$`)
}

View File

@ -0,0 +1,553 @@
package reference
import (
"regexp"
"strings"
"testing"
)
type regexpMatch struct {
input string
match bool
subs []string
}
func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) {
matches := r.FindStringSubmatch(m.input)
if m.match && matches != nil {
if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input {
t.Fatalf("Bad match result %#v for %q", matches, m.input)
}
if len(matches) < (len(m.subs) + 1) {
t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input)
}
for i := range m.subs {
if m.subs[i] != matches[i+1] {
t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input)
}
}
} else if m.match {
t.Errorf("Expected match for %q", m.input)
} else if matches != nil {
t.Errorf("Unexpected match for %q", m.input)
}
}
func TestDomainRegexp(t *testing.T) {
hostcases := []regexpMatch{
{
input: "test.com",
match: true,
},
{
input: "test.com:10304",
match: true,
},
{
input: "test.com:http",
match: false,
},
{
input: "localhost",
match: true,
},
{
input: "localhost:8080",
match: true,
},
{
input: "a",
match: true,
},
{
input: "a.b",
match: true,
},
{
input: "ab.cd.com",
match: true,
},
{
input: "a-b.com",
match: true,
},
{
input: "-ab.com",
match: false,
},
{
input: "ab-.com",
match: false,
},
{
input: "ab.c-om",
match: true,
},
{
input: "ab.-com",
match: false,
},
{
input: "ab.com-",
match: false,
},
{
input: "0101.com",
match: true, // TODO(dmcgowan): valid if this should be allowed
},
{
input: "001a.com",
match: true,
},
{
input: "b.gbc.io:443",
match: true,
},
{
input: "b.gbc.io",
match: true,
},
{
input: "xn--n3h.com", // ☃.com in punycode
match: true,
},
{
input: "Asdf.com", // uppercase character
match: true,
},
}
r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`)
for i := range hostcases {
checkRegexp(t, r, hostcases[i])
}
}
func TestFullNameRegexp(t *testing.T) {
if anchoredNameRegexp.NumSubexp() != 2 {
t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2",
anchoredNameRegexp, anchoredNameRegexp.NumSubexp())
}
testcases := []regexpMatch{
{
input: "",
match: false,
},
{
input: "short",
match: true,
subs: []string{"", "short"},
},
{
input: "simple/name",
match: true,
subs: []string{"simple", "name"},
},
{
input: "library/ubuntu",
match: true,
subs: []string{"library", "ubuntu"},
},
{
input: "docker/stevvooe/app",
match: true,
subs: []string{"docker", "stevvooe/app"},
},
{
input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"},
},
{
input: "aa/aa/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/bb/bb/bb"},
},
{
input: "a/a/a/a",
match: true,
subs: []string{"a", "a/a/a"},
},
{
input: "a/a/a/a/",
match: false,
},
{
input: "a//a/a",
match: false,
},
{
input: "a",
match: true,
subs: []string{"", "a"},
},
{
input: "a/aa",
match: true,
subs: []string{"a", "aa"},
},
{
input: "a/aa/a",
match: true,
subs: []string{"a", "aa/a"},
},
{
input: "foo.com",
match: true,
subs: []string{"", "foo.com"},
},
{
input: "foo.com/",
match: false,
},
{
input: "foo.com:8080/bar",
match: true,
subs: []string{"foo.com:8080", "bar"},
},
{
input: "foo.com:http/bar",
match: false,
},
{
input: "foo.com/bar",
match: true,
subs: []string{"foo.com", "bar"},
},
{
input: "foo.com/bar/baz",
match: true,
subs: []string{"foo.com", "bar/baz"},
},
{
input: "localhost:8080/bar",
match: true,
subs: []string{"localhost:8080", "bar"},
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
match: true,
subs: []string{"sub-dom1.foo.com", "bar/baz/quux"},
},
{
input: "blog.foo.com/bar/baz",
match: true,
subs: []string{"blog.foo.com", "bar/baz"},
},
{
input: "a^a",
match: false,
},
{
input: "aa/asdf$$^/aa",
match: false,
},
{
input: "asdf$$^/aa",
match: false,
},
{
input: "aa-a/a",
match: true,
subs: []string{"aa-a", "a"},
},
{
input: strings.Repeat("a/", 128) + "a",
match: true,
subs: []string{"a", strings.Repeat("a/", 127) + "a"},
},
{
input: "a-/a/a/a",
match: false,
},
{
input: "foo.com/a-/a/a",
match: false,
},
{
input: "-foo/bar",
match: false,
},
{
input: "foo/bar-",
match: false,
},
{
input: "foo-/bar",
match: false,
},
{
input: "foo/-bar",
match: false,
},
{
input: "_foo/bar",
match: false,
},
{
input: "foo_bar",
match: true,
subs: []string{"", "foo_bar"},
},
{
input: "foo_bar.com",
match: true,
subs: []string{"", "foo_bar.com"},
},
{
input: "foo_bar.com:8080",
match: false,
},
{
input: "foo_bar.com:8080/app",
match: false,
},
{
input: "foo.com/foo_bar",
match: true,
subs: []string{"foo.com", "foo_bar"},
},
{
input: "____/____",
match: false,
},
{
input: "_docker/_docker",
match: false,
},
{
input: "docker_/docker_",
match: false,
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
},
{
input: "xn--n3h.com/myimage", // ☃.com in punycode
match: true,
subs: []string{"xn--n3h.com", "myimage"},
},
{
input: "xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"xn--7o8h.com", "myimage"},
},
{
input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"example.com", "xn--7o8h.com/myimage"},
},
{
input: "example.com/some_separator__underscore/myimage",
match: true,
subs: []string{"example.com", "some_separator__underscore/myimage"},
},
{
input: "example.com/__underscore/myimage",
match: false,
},
{
input: "example.com/..dots/myimage",
match: false,
},
{
input: "example.com/.dots/myimage",
match: false,
},
{
input: "example.com/nodouble..dots/myimage",
match: false,
},
{
input: "example.com/nodouble..dots/myimage",
match: false,
},
{
input: "docker./docker",
match: false,
},
{
input: ".docker/docker",
match: false,
},
{
input: "docker-/docker",
match: false,
},
{
input: "-docker/docker",
match: false,
},
{
input: "do..cker/docker",
match: false,
},
{
input: "do__cker:8080/docker",
match: false,
},
{
input: "do__cker/docker",
match: true,
subs: []string{"", "do__cker/docker"},
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
},
{
input: "registry.io/foo/project--id.module--name.ver---sion--name",
match: true,
subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"},
},
{
input: "Asdf.com/foo/bar", // uppercase character in hostname
match: true,
},
{
input: "Foo/FarB", // uppercase characters in remote name
match: false,
},
}
for i := range testcases {
checkRegexp(t, anchoredNameRegexp, testcases[i])
}
}
func TestReferenceRegexp(t *testing.T) {
if ReferenceRegexp.NumSubexp() != 3 {
t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3",
ReferenceRegexp, ReferenceRegexp.NumSubexp())
}
testcases := []regexpMatch{
{
input: "registry.com:8080/myapp:tag",
match: true,
subs: []string{"registry.com:8080/myapp", "tag", ""},
},
{
input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@sha256:badbadbadbad",
match: false,
},
{
input: "registry.com:8080/myapp:invalid~tag",
match: false,
},
{
input: "bad_hostname.com:8080/myapp:tag",
match: false,
},
{
input:// localhost treated as name, missing tag with 8080 as tag
"localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: false,
},
{
// localhost will be treated as an image name without a host
input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@bad",
match: false,
},
{
input: "registry.com:8080/myapp@2bad",
match: false, // TODO(dmcgowan): Support this as valid
},
}
for i := range testcases {
checkRegexp(t, ReferenceRegexp, testcases[i])
}
}
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])
}
}