Merge pull request #963 from dmcgowan/reference-update
[Carry 690] Add a new reference package abstracting tags and digests
This commit is contained in:
commit
8aec18217a
12 changed files with 1342 additions and 371 deletions
316
reference/reference.go
Normal file
316
reference/reference.go
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
// 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 := hostcomponent [':' port-number]
|
||||||
|
// component := subcomponent [separator subcomponent]*
|
||||||
|
// subcomponent := alpha-numeric ['-'* alpha-numeric]*
|
||||||
|
// hostcomponent := [hostpart '.']* hostpart
|
||||||
|
// alpha-numeric := /[a-z0-9]+/
|
||||||
|
// separator := /([_.]|__)/
|
||||||
|
// port-number := /[0-9]+/
|
||||||
|
// hostpart := /([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-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 := /[0-9a-fA-F]{32,}/ ; Atleast 128 bit digest value
|
||||||
|
package reference
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/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")
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// RepositoryNameTotalLengthMax
|
||||||
|
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 hostname and digest
|
||||||
|
type Canonical interface {
|
||||||
|
Named
|
||||||
|
Digest() digest.Digest
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func SplitHostname(named Named) (string, string) {
|
||||||
|
name := named.Name()
|
||||||
|
match := anchoredNameRegexp.FindStringSubmatch(name)
|
||||||
|
if match == nil || len(match) != 3 {
|
||||||
|
return "", name
|
||||||
|
}
|
||||||
|
return match[1], match[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
// TODO(dmcgowan): Provide more specific and helpful error
|
||||||
|
return nil, ErrReferenceInvalidFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matches[1]) > NameTotalLengthMax {
|
||||||
|
return nil, ErrNameTooLong
|
||||||
|
}
|
||||||
|
|
||||||
|
ref := reference{
|
||||||
|
name: matches[1],
|
||||||
|
tag: matches[2],
|
||||||
|
}
|
||||||
|
if matches[3] != "" {
|
||||||
|
var err error
|
||||||
|
ref.digest, err = digest.ParseDigest(matches[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := getBestReferenceType(ref)
|
||||||
|
if r == nil {
|
||||||
|
return nil, ErrNameEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseNamed parses the input string and returns a named
|
||||||
|
// object representing the given string. If the input is
|
||||||
|
// invalid ErrReferenceInvalidFormat will be returned.
|
||||||
|
func ParseNamed(name string) (Named, error) {
|
||||||
|
if !anchoredNameRegexp.MatchString(name) {
|
||||||
|
return nil, ErrReferenceInvalidFormat
|
||||||
|
}
|
||||||
|
return repository(name), 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) (Tagged, error) {
|
||||||
|
if !anchoredNameRegexp.MatchString(tag) {
|
||||||
|
return nil, ErrTagInvalidFormat
|
||||||
|
}
|
||||||
|
return taggedReference{
|
||||||
|
name: name.Name(),
|
||||||
|
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) (Digested, error) {
|
||||||
|
if !anchoredDigestRegexp.MatchString(digest.String()) {
|
||||||
|
return nil, ErrDigestInvalidFormat
|
||||||
|
}
|
||||||
|
return canonicalReference{
|
||||||
|
name: name.Name(),
|
||||||
|
digest: digest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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{
|
||||||
|
name: ref.name,
|
||||||
|
digest: ref.digest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repository(ref.name)
|
||||||
|
}
|
||||||
|
if ref.digest == "" {
|
||||||
|
return taggedReference{
|
||||||
|
name: ref.name,
|
||||||
|
tag: ref.tag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
type reference struct {
|
||||||
|
name string
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reference) Tag() string {
|
||||||
|
return r.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r reference) Digest() digest.Digest {
|
||||||
|
return r.digest
|
||||||
|
}
|
||||||
|
|
||||||
|
type repository string
|
||||||
|
|
||||||
|
func (r repository) String() string {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r repository) Name() string {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type digestReference digest.Digest
|
||||||
|
|
||||||
|
func (d digestReference) String() string {
|
||||||
|
return d.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d digestReference) Digest() digest.Digest {
|
||||||
|
return digest.Digest(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
type taggedReference struct {
|
||||||
|
name string
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t taggedReference) String() string {
|
||||||
|
return t.name + ":" + t.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t taggedReference) Name() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t taggedReference) Tag() string {
|
||||||
|
return t.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
type canonicalReference struct {
|
||||||
|
name string
|
||||||
|
digest digest.Digest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c canonicalReference) String() string {
|
||||||
|
return c.name + "@" + c.digest.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c canonicalReference) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c canonicalReference) Digest() digest.Digest {
|
||||||
|
return c.digest
|
||||||
|
}
|
481
reference/reference_test.go
Normal file
481
reference/reference_test.go
Normal file
|
@ -0,0 +1,481 @@
|
||||||
|
package reference
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/distribution/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
|
||||||
|
// hostname is the hostname expected in the reference
|
||||||
|
hostname 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",
|
||||||
|
hostname: "test.com",
|
||||||
|
repository: "test.com/repo",
|
||||||
|
tag: "tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test:5000/repo",
|
||||||
|
hostname: "test:5000",
|
||||||
|
repository: "test:5000/repo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test:5000/repo:tag",
|
||||||
|
hostname: "test:5000",
|
||||||
|
repository: "test:5000/repo",
|
||||||
|
tag: "tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
hostname: "test:5000",
|
||||||
|
repository: "test:5000/repo",
|
||||||
|
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
hostname: "test:5000",
|
||||||
|
repository: "test:5000/repo",
|
||||||
|
tag: "tag",
|
||||||
|
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test:5000/repo",
|
||||||
|
hostname: "test:5000",
|
||||||
|
repository: "test:5000/repo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "",
|
||||||
|
err: ErrNameEmpty,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: ":justtag",
|
||||||
|
err: ErrReferenceInvalidFormat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
err: ErrReferenceInvalidFormat,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
err: digest.ErrDigestUnsupported,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: strings.Repeat("a/", 128) + "a:tag",
|
||||||
|
err: ErrNameTooLong,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
|
||||||
|
hostname: "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",
|
||||||
|
hostname: "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",
|
||||||
|
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",
|
||||||
|
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",
|
||||||
|
repository: "xn--n3h.com/myimage",
|
||||||
|
tag: "xn--n3h.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
|
||||||
|
hostname: "xn--7o8h.com",
|
||||||
|
repository: "xn--7o8h.com/myimage",
|
||||||
|
tag: "xn--7o8h.com",
|
||||||
|
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "foo_bar.com:8080",
|
||||||
|
repository: "foo_bar.com",
|
||||||
|
tag: "8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "foo/foo_bar.com:8080",
|
||||||
|
hostname: "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)
|
||||||
|
}
|
||||||
|
hostname, _ := SplitHostname(named)
|
||||||
|
if hostname != testcase.hostname {
|
||||||
|
failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname)
|
||||||
|
}
|
||||||
|
} else if testcase.repository != "" || testcase.hostname != "" {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitHostname(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
input string
|
||||||
|
hostname string
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "test.com/foo",
|
||||||
|
hostname: "test.com",
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test_com/foo",
|
||||||
|
hostname: "",
|
||||||
|
name: "test_com/foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test:8080/foo",
|
||||||
|
hostname: "test:8080",
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test.com:8080/foo",
|
||||||
|
hostname: "test.com:8080",
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "test-com:8080/foo",
|
||||||
|
hostname: "test-com:8080",
|
||||||
|
name: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "xn--n3h.com:18080/foo",
|
||||||
|
hostname: "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 := ParseNamed(testcase.input)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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:1234567890098765432112345667890098765",
|
||||||
|
name: "other.com/named",
|
||||||
|
digest: "sha256:1234567890098765432112345667890098765",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
failf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
named, err := ParseNamed(testcase.name)
|
||||||
|
if err != nil {
|
||||||
|
failf("error parsing name: %s", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
failf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
named, err := ParseNamed(testcase.name)
|
||||||
|
if err != nil {
|
||||||
|
failf("error parsing name: %s", err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
reference/regexp.go
Normal file
49
reference/regexp.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package reference
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// nameSubComponentRegexp defines the part of the name which must be
|
||||||
|
// begin and end with an alphanumeric character. These characters can
|
||||||
|
// be separated by any number of dashes.
|
||||||
|
nameSubComponentRegexp = regexp.MustCompile(`[a-z0-9]+(?:[-]+[a-z0-9]+)*`)
|
||||||
|
|
||||||
|
// 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, underscore or double underscore.
|
||||||
|
nameComponentRegexp = regexp.MustCompile(nameSubComponentRegexp.String() + `(?:(?:[._]|__)` + nameSubComponentRegexp.String() + `)*`)
|
||||||
|
|
||||||
|
nameRegexp = regexp.MustCompile(`(?:` + nameComponentRegexp.String() + `/)*` + nameComponentRegexp.String())
|
||||||
|
|
||||||
|
hostnameComponentRegexp = regexp.MustCompile(`(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])`)
|
||||||
|
|
||||||
|
// hostnameComponentRegexp restricts the registry hostname component of a repository name to
|
||||||
|
// start with a component as defined by hostnameRegexp and followed by an optional port.
|
||||||
|
hostnameRegexp = regexp.MustCompile(`(?:` + hostnameComponentRegexp.String() + `\.)*` + hostnameComponentRegexp.String() + `(?::[0-9]+)?`)
|
||||||
|
|
||||||
|
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
||||||
|
TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
||||||
|
|
||||||
|
// anchoredTagRegexp matches valid tag names, anchored at the start and
|
||||||
|
// end of the matched string.
|
||||||
|
anchoredTagRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`)
|
||||||
|
|
||||||
|
// DigestRegexp matches valid digests.
|
||||||
|
DigestRegexp = regexp.MustCompile(`[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 = regexp.MustCompile(`^` + DigestRegexp.String() + `$`)
|
||||||
|
|
||||||
|
// NameRegexp is the format for the name component of references. The
|
||||||
|
// regexp has capturing groups for the hostname and name part omitting
|
||||||
|
// the seperating forward slash from either.
|
||||||
|
NameRegexp = regexp.MustCompile(`(?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String())
|
||||||
|
|
||||||
|
// ReferenceRegexp is the full supported format of a reference. The
|
||||||
|
// regexp has capturing groups for name, tag, and digest components.
|
||||||
|
ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@](` + DigestRegexp.String() + `))?$`)
|
||||||
|
|
||||||
|
// anchoredNameRegexp is used to parse a name value, capturing hostname
|
||||||
|
anchoredNameRegexp = regexp.MustCompile(`^(?:(` + hostnameRegexp.String() + `)/)?(` + nameRegexp.String() + `)$`)
|
||||||
|
)
|
467
reference/regexp_test.go
Normal file
467
reference/regexp_test.go
Normal file
|
@ -0,0 +1,467 @@
|
||||||
|
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 TestHostRegexp(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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r := regexp.MustCompile(`^` + hostnameRegexp.String() + `$`)
|
||||||
|
for i := range hostcases {
|
||||||
|
checkRegexp(t, r, hostcases[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFullNameRegexp(t *testing.T) {
|
||||||
|
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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i := range testcases {
|
||||||
|
checkRegexp(t, anchoredNameRegexp, testcases[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceRegexp(t *testing.T) {
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/api/errcode"
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -12,7 +13,7 @@ var (
|
||||||
nameParameterDescriptor = ParameterDescriptor{
|
nameParameterDescriptor = ParameterDescriptor{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Format: RepositoryNameRegexp.String(),
|
Format: reference.NameRegexp.String(),
|
||||||
Required: true,
|
Required: true,
|
||||||
Description: `Name of the target repository.`,
|
Description: `Name of the target repository.`,
|
||||||
}
|
}
|
||||||
|
@ -20,7 +21,7 @@ var (
|
||||||
referenceParameterDescriptor = ParameterDescriptor{
|
referenceParameterDescriptor = ParameterDescriptor{
|
||||||
Name: "reference",
|
Name: "reference",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Format: TagNameRegexp.String(),
|
Format: reference.TagRegexp.String(),
|
||||||
Required: true,
|
Required: true,
|
||||||
Description: `Tag or digest of the target manifest.`,
|
Description: `Tag or digest of the target manifest.`,
|
||||||
}
|
}
|
||||||
|
@ -389,7 +390,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: RouteNameTags,
|
Name: RouteNameTags,
|
||||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list",
|
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list",
|
||||||
Entity: "Tags",
|
Entity: "Tags",
|
||||||
Description: "Retrieve information about tags.",
|
Description: "Retrieve information about tags.",
|
||||||
Methods: []MethodDescriptor{
|
Methods: []MethodDescriptor{
|
||||||
|
@ -517,7 +518,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: RouteNameManifest,
|
Name: RouteNameManifest,
|
||||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/manifests/{reference:" + TagNameRegexp.String() + "|" + digest.DigestRegexp.String() + "}",
|
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}",
|
||||||
Entity: "Manifest",
|
Entity: "Manifest",
|
||||||
Description: "Create, update, delete and retrieve manifests.",
|
Description: "Create, update, delete and retrieve manifests.",
|
||||||
Methods: []MethodDescriptor{
|
Methods: []MethodDescriptor{
|
||||||
|
@ -782,7 +783,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
|
|
||||||
{
|
{
|
||||||
Name: RouteNameBlob,
|
Name: RouteNameBlob,
|
||||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
|
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}",
|
||||||
Entity: "Blob",
|
Entity: "Blob",
|
||||||
Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.",
|
Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.",
|
||||||
Methods: []MethodDescriptor{
|
Methods: []MethodDescriptor{
|
||||||
|
@ -1006,7 +1007,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
|
|
||||||
{
|
{
|
||||||
Name: RouteNameBlobUpload,
|
Name: RouteNameBlobUpload,
|
||||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/",
|
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/",
|
||||||
Entity: "Initiate Blob Upload",
|
Entity: "Initiate Blob Upload",
|
||||||
Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.",
|
Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.",
|
||||||
Methods: []MethodDescriptor{
|
Methods: []MethodDescriptor{
|
||||||
|
@ -1128,7 +1129,7 @@ var routeDescriptors = []RouteDescriptor{
|
||||||
|
|
||||||
{
|
{
|
||||||
Name: RouteNameBlobUploadChunk,
|
Name: RouteNameBlobUploadChunk,
|
||||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}",
|
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}",
|
||||||
Entity: "Blob Upload",
|
Entity: "Blob Upload",
|
||||||
Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.",
|
Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.",
|
||||||
Methods: []MethodDescriptor{
|
Methods: []MethodDescriptor{
|
||||||
|
|
|
@ -1,96 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO(stevvooe): Move these definitions to the future "reference" package.
|
|
||||||
// While they are used with v2 definitions, their relevance expands beyond.
|
|
||||||
|
|
||||||
const (
|
|
||||||
// RepositoryNameTotalLengthMax is the maximum total number of characters in
|
|
||||||
// a repository name
|
|
||||||
RepositoryNameTotalLengthMax = 255
|
|
||||||
)
|
|
||||||
|
|
||||||
// domainLabelRegexp represents the following RFC-2396 BNF construct:
|
|
||||||
// domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
|
|
||||||
var domainLabelRegexp = regexp.MustCompile(`[a-z0-9](?:-*[a-z0-9])*`)
|
|
||||||
|
|
||||||
// RepositoryNameComponentRegexp restricts registry path component names to
|
|
||||||
// the allow valid hostnames according to: https://www.ietf.org/rfc/rfc2396.txt
|
|
||||||
// with the following differences:
|
|
||||||
// 1) It DOES NOT allow for fully-qualified domain names, which include a
|
|
||||||
// trailing '.', e.g. "google.com."
|
|
||||||
// 2) It DOES NOT restrict 'top-level' domain labels to start with just alpha
|
|
||||||
// characters.
|
|
||||||
// 3) It DOES allow for underscores to appear in the same situations as dots.
|
|
||||||
//
|
|
||||||
// RFC-2396 uses the BNF construct:
|
|
||||||
// hostname = *( domainlabel "." ) toplabel [ "." ]
|
|
||||||
var RepositoryNameComponentRegexp = regexp.MustCompile(
|
|
||||||
domainLabelRegexp.String() + `(?:[._]` + domainLabelRegexp.String() + `)*`)
|
|
||||||
|
|
||||||
// RepositoryNameComponentAnchoredRegexp is the version of
|
|
||||||
// RepositoryNameComponentRegexp which must completely match the content
|
|
||||||
var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`)
|
|
||||||
|
|
||||||
// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow
|
|
||||||
// multiple path components, separated by a forward slash.
|
|
||||||
var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String())
|
|
||||||
|
|
||||||
// TagNameRegexp matches valid tag names. From docker/docker:graph/tags.go.
|
|
||||||
var TagNameRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`)
|
|
||||||
|
|
||||||
// TagNameAnchoredRegexp matches valid tag names, anchored at the start and
|
|
||||||
// end of the matched string.
|
|
||||||
var TagNameAnchoredRegexp = regexp.MustCompile("^" + TagNameRegexp.String() + "$")
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrRepositoryNameEmpty is returned for empty, invalid repository names.
|
|
||||||
ErrRepositoryNameEmpty = fmt.Errorf("repository name must have at least one component")
|
|
||||||
|
|
||||||
// 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())
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidateRepositoryName 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:
|
|
||||||
//
|
|
||||||
// alpha-numeric := /[a-z0-9]+/
|
|
||||||
// separator := /[._-]/
|
|
||||||
// component := alpha-numeric [separator alpha-numeric]*
|
|
||||||
// namespace := component ['/' component]*
|
|
||||||
//
|
|
||||||
// The result of the production, known as the "namespace", should be limited
|
|
||||||
// to 255 characters.
|
|
||||||
func ValidateRepositoryName(name string) error {
|
|
||||||
if name == "" {
|
|
||||||
return ErrRepositoryNameEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(name) > RepositoryNameTotalLengthMax {
|
|
||||||
return ErrRepositoryNameLong
|
|
||||||
}
|
|
||||||
|
|
||||||
components := strings.Split(name, "/")
|
|
||||||
|
|
||||||
for _, component := range components {
|
|
||||||
if !RepositoryNameComponentAnchoredRegexp.MatchString(component) {
|
|
||||||
return ErrRepositoryNameComponentInvalid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,256 +0,0 @@
|
||||||
package v2
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "short",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "a/aa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "aa/a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "a/aa/a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo.com/",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// TODO: this testcase should be valid once we switch to
|
|
||||||
// the reference package.
|
|
||||||
input: "foo.com:8080/bar",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo.com/bar",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo.com/bar/baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo.com/bar/baz/quux",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "blog.foo.com/bar/baz",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "asdf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "asdf$$^/aa",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "aa-a/aa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "aa/aa",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "a-a/a-a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "a-/a/a/a",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: strings.Repeat("a", 255),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: strings.Repeat("a", 256),
|
|
||||||
err: ErrRepositoryNameLong,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "-foo/bar",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo/bar-",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo-/bar",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo/-bar",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "_foo/bar",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "foo/bar_",
|
|
||||||
err: ErrRepositoryNameComponentInvalid,
|
|
||||||
invalid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
input: "____/____",
|
|
||||||
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,
|
|
||||||
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: "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 := ValidateRepositoryName(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) {
|
|
||||||
for _, testcase := range regexpTestcases {
|
|
||||||
failf := func(format string, v ...interface{}) {
|
|
||||||
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
|
|
||||||
matches := RepositoryNameRegexp.FindString(testcase.input) == 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -170,6 +170,14 @@ func TestRouter(t *testing.T) {
|
||||||
"name": "foo/bar/manifests",
|
"name": "foo/bar/manifests",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
RouteName: RouteNameManifest,
|
||||||
|
RequestURI: "/v2/locahost:8080/foo/bar/baz/manifests/tag",
|
||||||
|
Vars: map[string]string{
|
||||||
|
"name": "locahost:8080/foo/bar/baz",
|
||||||
|
"reference": "tag",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTestRouter(t, testCases, "", true)
|
checkTestRouter(t, testCases, "", true)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/docker/distribution/context"
|
"github.com/docker/distribution/context"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/docker/distribution/manifest/schema1"
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
"github.com/docker/distribution/registry/api/v2"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/distribution/registry/storage/cache"
|
"github.com/docker/distribution/registry/storage/cache"
|
||||||
|
@ -96,9 +97,9 @@ func (r *registry) Repositories(ctx context.Context, entries []string, last stri
|
||||||
return numFilled, returnErr
|
return numFilled, returnErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRepository creates a new Repository for the given repository name and base URL
|
// NewRepository creates a new Repository for the given repository name and base URL.
|
||||||
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
|
func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
|
||||||
if err := v2.ValidateRepositoryName(name); err != nil {
|
if _, err := reference.ParseNamed(name); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
registry/storage/cache/memory/memory.go
vendored
4
registry/storage/cache/memory/memory.go
vendored
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/context"
|
"github.com/docker/distribution/context"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/storage/cache"
|
"github.com/docker/distribution/registry/storage/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
|
func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
|
||||||
if err := v2.ValidateRepositoryName(repo); err != nil {
|
if _, err := reference.ParseNamed(repo); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
4
registry/storage/cache/redis/redis.go
vendored
4
registry/storage/cache/redis/redis.go
vendored
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/context"
|
"github.com/docker/distribution/context"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/storage/cache"
|
"github.com/docker/distribution/registry/storage/cache"
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/garyburd/redigo/redis"
|
||||||
)
|
)
|
||||||
|
@ -41,7 +41,7 @@ func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorC
|
||||||
|
|
||||||
// RepositoryScoped returns the scoped cache.
|
// RepositoryScoped returns the scoped cache.
|
||||||
func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
|
func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) {
|
||||||
if err := v2.ValidateRepositoryName(repo); err != nil {
|
if _, err := reference.ParseNamed(repo); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package storage
|
||||||
import (
|
import (
|
||||||
"github.com/docker/distribution"
|
"github.com/docker/distribution"
|
||||||
"github.com/docker/distribution/context"
|
"github.com/docker/distribution/context"
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/distribution/registry/storage/cache"
|
"github.com/docker/distribution/registry/storage/cache"
|
||||||
storagedriver "github.com/docker/distribution/registry/storage/driver"
|
storagedriver "github.com/docker/distribution/registry/storage/driver"
|
||||||
)
|
)
|
||||||
|
@ -107,10 +107,10 @@ func (reg *registry) Scope() distribution.Scope {
|
||||||
// Repository returns an instance of the repository tied to the registry.
|
// Repository returns an instance of the repository tied to the registry.
|
||||||
// Instances should not be shared between goroutines but are cheap to
|
// Instances should not be shared between goroutines but are cheap to
|
||||||
// allocate. In general, they should be request scoped.
|
// allocate. In general, they should be request scoped.
|
||||||
func (reg *registry) Repository(ctx context.Context, name string) (distribution.Repository, error) {
|
func (reg *registry) Repository(ctx context.Context, canonicalName string) (distribution.Repository, error) {
|
||||||
if err := v2.ValidateRepositoryName(name); err != nil {
|
if _, err := reference.ParseNamed(canonicalName); err != nil {
|
||||||
return nil, distribution.ErrRepositoryNameInvalid{
|
return nil, distribution.ErrRepositoryNameInvalid{
|
||||||
Name: name,
|
Name: canonicalName,
|
||||||
Reason: err,
|
Reason: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
|
||||||
var descriptorCache distribution.BlobDescriptorService
|
var descriptorCache distribution.BlobDescriptorService
|
||||||
if reg.blobDescriptorCacheProvider != nil {
|
if reg.blobDescriptorCacheProvider != nil {
|
||||||
var err error
|
var err error
|
||||||
descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(name)
|
descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(canonicalName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
|
||||||
return &repository{
|
return &repository{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
registry: reg,
|
registry: reg,
|
||||||
name: name,
|
name: canonicalName,
|
||||||
descriptorCache: descriptorCache,
|
descriptorCache: descriptorCache,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue