Add a new reference package abstracting repositories, tags and digests
There seems to be a need for a type that represents a way of pointing to an image, irrespective of the implementation. This patch defines a Reference interface and provides 3 implementations: - TagReference: when only a tag is provided - DigestReference: when a digest (according to the digest package) is provided, can include optional tag as well Validation of references are purely syntactic. There is also a strong type for tags, analogous to digests, as well as a strong type for Repository from which clients can access the hostname alone, or the repository name without the hostname, or both together via the String() method. For Repository, the files names.go and names_test.go were moved from the v2 package. Signed-off-by: Tibor Vass <tibor@docker.com>
This commit is contained in:
parent
1d266b00e1
commit
f8c09b6a7d
11 changed files with 495 additions and 143 deletions
186
reference/reference.go
Normal file
186
reference/reference.go
Normal file
|
@ -0,0 +1,186 @@
|
|||
// 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 := repository [ ":" tag ] [ "@" digest ]
|
||||
//
|
||||
// // repository.go
|
||||
// repository := hostname ['/' component]+
|
||||
// hostname := component [':' port-number]
|
||||
// component := alpha-numeric [separator alpha-numeric]*
|
||||
// alpha-numeric := /[a-zA-Z0-9]+/
|
||||
// separator := /[._-]/
|
||||
// port-number := /[0-9]+/
|
||||
//
|
||||
// // tag.go
|
||||
// tag := /[\w][\w.-]{0,127}/
|
||||
//
|
||||
// // from the digest package
|
||||
// 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 := /[A-Za-z0-9_-]+/ ; supports hex bytes or url safe base64
|
||||
package reference
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
)
|
||||
|
||||
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
|
||||
var ErrReferenceInvalidFormat = errors.New("invalid reference format")
|
||||
|
||||
// Reference abstracts types that reference images in a certain way.
|
||||
type Reference interface {
|
||||
// Repository returns the repository part of a reference
|
||||
Repository() Repository
|
||||
// String returns the entire reference, including the repository part
|
||||
String() string
|
||||
}
|
||||
|
||||
func parseHostname(s string) (hostname, tail string) {
|
||||
tail = s
|
||||
i := regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String()).FindStringIndex(s)
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
return s[:i[1]], s[i[1]:]
|
||||
}
|
||||
|
||||
func parseRepositoryName(s string) (repo, tail string) {
|
||||
tail = s
|
||||
i := regexp.MustCompile(`^/(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()).FindStringIndex(s)
|
||||
if i == nil {
|
||||
return
|
||||
}
|
||||
return s[:i[1]], s[i[1]:]
|
||||
}
|
||||
|
||||
func parseTag(s string) (tag Tag, tail string) {
|
||||
tail = s
|
||||
if len(s) == 0 || s[0] != ':' {
|
||||
return
|
||||
}
|
||||
tag, err := NewTag(s[1:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tail = s[len(tag)+1:]
|
||||
return
|
||||
}
|
||||
|
||||
func parseDigest(s string) (dgst digest.Digest, tail string) {
|
||||
tail = s
|
||||
if len(s) == 0 || s[0] != '@' {
|
||||
return
|
||||
}
|
||||
dgst, err := digest.ParseDigest(s[1:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tail = s[len(dgst)+1:]
|
||||
return
|
||||
}
|
||||
|
||||
// Parse parses s and returns a syntactically valid Reference.
|
||||
// If an error was encountered it is returned, along with a nil Reference.
|
||||
func Parse(s string) (Reference, error) {
|
||||
hostname, s := parseHostname(s)
|
||||
name, s := parseRepositoryName(s)
|
||||
repository := Repository{Hostname: hostname, Name: name}
|
||||
if err := repository.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tag, s := parseTag(s)
|
||||
dgst, s := parseDigest(s)
|
||||
if len(s) > 0 {
|
||||
return nil, ErrReferenceInvalidFormat
|
||||
}
|
||||
|
||||
if dgst != "" {
|
||||
return DigestReference{repository: repository, digest: dgst, tag: tag}, nil
|
||||
}
|
||||
if tag != "" {
|
||||
return TagReference{repository: repository, tag: tag}, nil
|
||||
}
|
||||
return nil, ErrReferenceInvalidFormat
|
||||
}
|
||||
|
||||
// DigestReference represents a reference of the form `repository@sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef`.
|
||||
// Implements the Reference interface.
|
||||
type DigestReference struct {
|
||||
repository Repository
|
||||
digest digest.Digest
|
||||
tag Tag
|
||||
}
|
||||
|
||||
// Repository returns the repository part.
|
||||
func (r DigestReference) Repository() Repository { return r.repository }
|
||||
|
||||
// String returns the full string reference.
|
||||
func (r DigestReference) String() string {
|
||||
return r.repository.String() + "@" + string(r.digest)
|
||||
}
|
||||
|
||||
// NewDigestReference returns an initialized DigestReference.
|
||||
func NewDigestReference(canonicalRepository string, digest digest.Digest, optionalTag Tag) (DigestReference, error) {
|
||||
ref := DigestReference{}
|
||||
|
||||
repo, err := NewRepository(canonicalRepository)
|
||||
if err != nil {
|
||||
return ref, err
|
||||
}
|
||||
ref.repository = repo
|
||||
|
||||
if err := digest.Validate(); err != nil {
|
||||
return ref, err
|
||||
}
|
||||
ref.digest = digest
|
||||
|
||||
if len(optionalTag) > 0 {
|
||||
if err := optionalTag.Validate(); err != nil {
|
||||
return ref, err
|
||||
}
|
||||
ref.tag = optionalTag
|
||||
}
|
||||
|
||||
return ref, err
|
||||
}
|
||||
|
||||
// TagReference represents a reference of the form `repository:tag`.
|
||||
// Implements the Reference interface.
|
||||
type TagReference struct {
|
||||
repository Repository
|
||||
tag Tag
|
||||
}
|
||||
|
||||
// Repository returns the repository part.
|
||||
func (r TagReference) Repository() Repository { return r.repository }
|
||||
|
||||
// String returns the full string reference.
|
||||
func (r TagReference) String() string {
|
||||
return r.repository.String() + ":" + string(r.tag)
|
||||
}
|
||||
|
||||
// NewTagReference returns an initialized TagReference.
|
||||
func NewTagReference(canonicalRepository string, tagName string) (TagReference, error) {
|
||||
ref := TagReference{}
|
||||
|
||||
repo, err := NewRepository(canonicalRepository)
|
||||
if err != nil {
|
||||
return ref, err
|
||||
}
|
||||
ref.repository = repo
|
||||
|
||||
tag, err := NewTag(tagName)
|
||||
if err != nil {
|
||||
return ref, err
|
||||
}
|
||||
ref.tag = tag
|
||||
|
||||
return ref, err
|
||||
}
|
56
reference/reference_test.go
Normal file
56
reference/reference_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package reference
|
||||
|
||||
/*
|
||||
var refRegex = regexp.MustCompile(`^([a-z0-9]+(?:[-._][a-z0-9]+)*(?::[0-9]+(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+|(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+)?)(:[\w][\w.-]{0,127})?(@` + digest.DigestRegexp.String() + `)?$`)
|
||||
|
||||
func getRepo(s string) string {
|
||||
matches := refRegex.FindStringSubmatch(s)
|
||||
if len(matches) == 0 {
|
||||
return ""
|
||||
}
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
func testRepository(prefix string) error {
|
||||
for _, s := range []string{
|
||||
prefix + `@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`,
|
||||
prefix + `:frozen@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`,
|
||||
prefix + `:latest`,
|
||||
prefix,
|
||||
} {
|
||||
expected := getRepo(s)
|
||||
ref, err := Parse(s)
|
||||
if err != nil {
|
||||
if expected == "" {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if repo := ref.Repository(); repo.String() != expected {
|
||||
return fmt.Errorf("repository string: expected %q, got: %q", expected, repo)
|
||||
}
|
||||
if refStr := ref.String(); refStr != s {
|
||||
return fmt.Errorf("reference string: expected %q, got: %q", s, refStr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSimpleRepository(t *testing.T) {
|
||||
if err := testRepository(`busybox`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlRepository(t *testing.T) {
|
||||
if err := testRepository(`docker.io/library/busybox`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPort(t *testing.T) {
|
||||
if err := testRepository(`busybox:1234`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
*/
|
136
reference/repository.go
Normal file
136
reference/repository.go
Normal file
|
@ -0,0 +1,136 @@
|
|||
package reference
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name.
|
||||
RepositoryNameTotalLengthMax = 255
|
||||
)
|
||||
|
||||
// RepositoryNameComponentRegexp restricts registry path component names to
|
||||
// start with at least one letter or number, with following parts able to
|
||||
// be separated by one period, dash or underscore.
|
||||
var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`)
|
||||
|
||||
// RepositoryNameComponentAnchoredRegexp is the version of
|
||||
// RepositoryNameComponentRegexp which must completely match the content
|
||||
var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
|
||||
|
||||
// RepositoryNameHostnameRegexp restricts the registry hostname component of a repository name to
|
||||
// start with a component as defined by RepositoryNameComponentRegexp and followed by an optional port.
|
||||
var RepositoryNameHostnameRegexp = regexp.MustCompile(RepositoryNameComponentRegexp.String() + `(?::[0-9]+)?`)
|
||||
|
||||
// RepositoryNameHostnameAnchoredRegexp is the version of
|
||||
// RepositoryNameHostnameRegexp which must completely match the content.
|
||||
var RepositoryNameHostnameAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String() + `$`)
|
||||
|
||||
// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
|
||||
// multiple path components, separated by a forward slash.
|
||||
var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameHostnameRegexp.String() + `/)?(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String())
|
||||
|
||||
var (
|
||||
// ErrRepositoryNameEmpty is returned for empty, invalid repository names.
|
||||
ErrRepositoryNameEmpty = errors.New("repository name must have at least one component")
|
||||
|
||||
// ErrRepositoryNameMissingHostname is returned when a repository name
|
||||
// does not start with a hostname
|
||||
ErrRepositoryNameMissingHostname = errors.New("repository name must start with a hostname")
|
||||
|
||||
// ErrRepositoryNameHostnameInvalid is returned when a repository name
|
||||
// does not match RepositoryNameHostnameRegexp
|
||||
ErrRepositoryNameHostnameInvalid = fmt.Errorf("repository name must match %q", RepositoryNameHostnameRegexp.String())
|
||||
|
||||
// ErrRepositoryNameLong is returned when a repository name is longer than
|
||||
// RepositoryNameTotalLengthMax
|
||||
ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax)
|
||||
|
||||
// ErrRepositoryNameComponentInvalid is returned when a repository name does
|
||||
// not match RepositoryNameComponentRegexp
|
||||
ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String())
|
||||
)
|
||||
|
||||
// Repository represents a reference to a Repository.
|
||||
type Repository struct {
|
||||
// Hostname refers to the registry hostname where the repository resides.
|
||||
Hostname string
|
||||
// Name is a slash (`/`) separated list of string components.
|
||||
Name string
|
||||
}
|
||||
|
||||
// String returns the string representation of a repository.
|
||||
func (r Repository) String() string {
|
||||
// Hostname is not supposed to be empty, but let's be nice.
|
||||
if len(r.Hostname) == 0 {
|
||||
return r.Name
|
||||
}
|
||||
return r.Hostname + "/" + r.Name
|
||||
}
|
||||
|
||||
// Validate ensures the repository name is valid for use in the
|
||||
// registry. This function accepts a superset of what might be accepted by
|
||||
// docker core or docker hub. If the name does not pass validation, an error,
|
||||
// describing the conditions, is returned.
|
||||
//
|
||||
// Effectively, the name should comply with the following grammar:
|
||||
//
|
||||
// repository := hostname ['/' component]+
|
||||
// hostname := component [':' port-number]
|
||||
// component := alpha-numeric [separator alpha-numeric]*
|
||||
// alpha-numeric := /[a-zA-Z0-9]+/
|
||||
// separator := /[._-]/
|
||||
// port-number := /[0-9]+/
|
||||
//
|
||||
// The result of the production should be limited to 255 characters.
|
||||
func (r Repository) Validate() error {
|
||||
n := len(r.String())
|
||||
switch {
|
||||
case n == 0:
|
||||
return ErrRepositoryNameEmpty
|
||||
case n > RepositoryNameTotalLengthMax:
|
||||
return ErrRepositoryNameLong
|
||||
case len(r.Hostname) <= 0:
|
||||
return ErrRepositoryNameMissingHostname
|
||||
case !RepositoryNameHostnameAnchoredRegexp.MatchString(r.Hostname):
|
||||
return ErrRepositoryNameHostnameInvalid
|
||||
}
|
||||
|
||||
components := r.Name
|
||||
for {
|
||||
var component string
|
||||
sep := strings.Index(components, "/")
|
||||
if sep >= 0 {
|
||||
component = components[:sep]
|
||||
components = components[sep+1:]
|
||||
} else { // if no more slashes
|
||||
component = components
|
||||
components = ""
|
||||
}
|
||||
if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
|
||||
return ErrRepositoryNameComponentInvalid
|
||||
}
|
||||
if sep < 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepository returns a valid Repository from an input string representing
|
||||
// the canonical form of a repository name.
|
||||
// If the validation fails, an error is returned.
|
||||
func NewRepository(canonicalName string) (repo Repository, err error) {
|
||||
if len(canonicalName) == 0 {
|
||||
return repo, ErrRepositoryNameEmpty
|
||||
}
|
||||
i := strings.Index(canonicalName, "/")
|
||||
if i <= 0 {
|
||||
return repo, ErrRepositoryNameMissingHostname
|
||||
}
|
||||
repo.Hostname = canonicalName[:i]
|
||||
repo.Name = canonicalName[i+1:]
|
||||
return repo, repo.Validate()
|
||||
}
|
286
reference/repository_test.go
Normal file
286
reference/repository_test.go
Normal file
|
@ -0,0 +1,286 @@
|
|||
package reference
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
// regexpTestcases is a unified set of testcases for
|
||||
// TestValidateRepositoryName and TestRepositoryNameRegexp.
|
||||
// Some of them are valid inputs for one and not the other.
|
||||
regexpTestcases = []struct {
|
||||
// input is the repository name or name component testcase
|
||||
input string
|
||||
// err is the error expected from ValidateRepositoryName, or nil
|
||||
err error
|
||||
// invalid should be true if the testcase is *not* expected to
|
||||
// match RepositoryNameRegexp
|
||||
invalid bool
|
||||
}{
|
||||
{
|
||||
input: "",
|
||||
err: ErrRepositoryNameEmpty,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "short",
|
||||
err: ErrRepositoryNameMissingHostname,
|
||||
},
|
||||
{
|
||||
input: "simple/name",
|
||||
},
|
||||
{
|
||||
input: "library/ubuntu",
|
||||
},
|
||||
{
|
||||
input: "docker/stevvooe/app",
|
||||
},
|
||||
{
|
||||
input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
|
||||
},
|
||||
{
|
||||
input: "aa/aa/bb/bb/bb",
|
||||
},
|
||||
{
|
||||
input: "a/a/a/b/b",
|
||||
},
|
||||
{
|
||||
input: "a/a/a/a/",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "a//a/a",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "a",
|
||||
err: ErrRepositoryNameMissingHostname,
|
||||
},
|
||||
{
|
||||
input: "a/aa",
|
||||
},
|
||||
{
|
||||
input: "aa/a",
|
||||
},
|
||||
{
|
||||
input: "a/aa/a",
|
||||
},
|
||||
{
|
||||
input: "foo.com/",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "foo.com:8080/bar",
|
||||
},
|
||||
{
|
||||
input: "foo.com/bar",
|
||||
},
|
||||
{
|
||||
input: "foo.com/bar/baz",
|
||||
},
|
||||
{
|
||||
input: "foo.com/bar/baz/quux",
|
||||
},
|
||||
{
|
||||
input: "blog.foo.com/bar/baz",
|
||||
},
|
||||
{
|
||||
input: "asdf",
|
||||
err: ErrRepositoryNameMissingHostname,
|
||||
},
|
||||
{
|
||||
input: "aa/asdf$$^/aa",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "asdf$$^/aa",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "aa-a/aa",
|
||||
},
|
||||
{
|
||||
input: "aa/aa",
|
||||
},
|
||||
{
|
||||
input: "a-a/a-a",
|
||||
},
|
||||
{
|
||||
input: "a",
|
||||
err: ErrRepositoryNameMissingHostname,
|
||||
},
|
||||
{
|
||||
input: "a/image",
|
||||
},
|
||||
{
|
||||
input: "a-/a/a/a",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "a/a-/a/a/a",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
// total length = 255
|
||||
input: "a/" + strings.Repeat("a", 253),
|
||||
},
|
||||
{
|
||||
// total length = 256
|
||||
input: "b/" + strings.Repeat("a", 254),
|
||||
err: ErrRepositoryNameLong,
|
||||
},
|
||||
{
|
||||
input: "-foo/bar",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "foo/bar-",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "foo-/bar",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "foo/-bar",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "_foo/bar",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "foo/bar_",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "____/____",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "_docker/_docker",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "docker_/docker_",
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "do__cker/docker",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "docker./docker",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: ".docker/docker",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "do..cker/docker",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "docker-/docker",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "-docker/docker",
|
||||
err: ErrRepositoryNameComponentInvalid,
|
||||
},
|
||||
{
|
||||
input: "xn--n3h.com/myimage", // http://☃.com in punycode
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "xn--7o8h.com/myimage", // http://🐳.com in punycode
|
||||
err: ErrRepositoryNameHostnameInvalid,
|
||||
invalid: true,
|
||||
},
|
||||
{
|
||||
input: "b.gcr.io/test.example.com/my-app", // embedded domain component
|
||||
},
|
||||
{
|
||||
input: "xn--n3h.com/myimage", // http://☃.com in punycode
|
||||
},
|
||||
{
|
||||
input: "xn--7o8h.com/myimage", // http://🐳.com in punycode
|
||||
},
|
||||
{
|
||||
input: "registry.io/foo/project--id.module--name.ver---sion--name", // image with hostname
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// TestValidateRepositoryName tests the ValidateRepositoryName function,
|
||||
// which uses RepositoryNameComponentAnchoredRegexp for validation
|
||||
func TestValidateRepositoryName(t *testing.T) {
|
||||
for _, testcase := range regexpTestcases {
|
||||
failf := func(format string, v ...interface{}) {
|
||||
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
if _, err := NewRepository(testcase.input); err != testcase.err {
|
||||
if testcase.err != nil {
|
||||
if err != nil {
|
||||
failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err)
|
||||
} else {
|
||||
failf("expected invalid repository: %v", testcase.err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
// Wrong error returned.
|
||||
failf("unexpected error validating repository name: %v, expected %v", err, testcase.err)
|
||||
} else {
|
||||
failf("unexpected error validating repository name: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepositoryNameRegexp(t *testing.T) {
|
||||
AnchoredRepositoryNameRegexp := regexp.MustCompile(`^` + RepositoryNameRegexp.String() + `$`)
|
||||
for _, testcase := range regexpTestcases {
|
||||
failf := func(format string, v ...interface{}) {
|
||||
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
matches := AnchoredRepositoryNameRegexp.MatchString(testcase.input)
|
||||
if matches == testcase.invalid {
|
||||
if testcase.invalid {
|
||||
failf("expected invalid repository name %s", testcase.input)
|
||||
} else {
|
||||
failf("expected valid repository name %s", testcase.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
38
reference/tag.go
Normal file
38
reference/tag.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package reference
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||
TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
||||
|
||||
// TagAnchoredRegexp matches valid tag names, anchored at the start and
|
||||
// end of the matched string.
|
||||
TagAnchoredRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`)
|
||||
|
||||
// ErrTagInvalid is returned when a tag does not match TagAnchoredRegexp.
|
||||
ErrTagInvalid = fmt.Errorf("tag name must match %q", TagRegexp.String())
|
||||
)
|
||||
|
||||
// Tag represents an image's tag name.
|
||||
type Tag string
|
||||
|
||||
// NewTag returns a valid Tag from an input string s.
|
||||
// If the validation fails, an error is returned.
|
||||
func NewTag(s string) (Tag, error) {
|
||||
tag := Tag(s)
|
||||
return tag, tag.Validate()
|
||||
}
|
||||
|
||||
// Validate returns ErrTagInvalid if tag does not match TagAnchoredRegexp.
|
||||
//
|
||||
// tag := [\w][\w.-]{0,127}
|
||||
func (tag Tag) Validate() error {
|
||||
if !TagAnchoredRegexp.MatchString(string(tag)) {
|
||||
return ErrTagInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue