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. Update regexp to support repeated dash and double underscore Add field type for serialization Since reference itself may be represented by multiple types which implement the reference inteface, serialization can lead to ambiguous type which cannot be deserialized. Field wraps the reference object to ensure that the correct type is always deserialized, requiring an extra unwrap of the reference after deserialization. Signed-off-by: Tibor Vass <tibor@docker.com> Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
parent
20c4b7a180
commit
ddfd2335c7
12 changed files with 1222 additions and 372 deletions
286
reference/reference.go
Normal file
286
reference/reference.go
Normal file
|
@ -0,0 +1,286 @@
|
|||
// 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")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
397
reference/reference_test.go
Normal file
397
reference/reference_test.go
Normal file
|
@ -0,0 +1,397 @@
|
|||
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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
42
reference/regexp.go
Normal file
42
reference/regexp.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
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() + `$`)
|
||||
|
||||
// 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() + `))?(?:[@]([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$`)
|
||||
|
||||
// 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"
|
||||
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/api/errcode"
|
||||
)
|
||||
|
||||
|
@ -12,7 +13,7 @@ var (
|
|||
nameParameterDescriptor = ParameterDescriptor{
|
||||
Name: "name",
|
||||
Type: "string",
|
||||
Format: RepositoryNameRegexp.String(),
|
||||
Format: reference.NameRegexp.String(),
|
||||
Required: true,
|
||||
Description: `Name of the target repository.`,
|
||||
}
|
||||
|
@ -20,7 +21,7 @@ var (
|
|||
referenceParameterDescriptor = ParameterDescriptor{
|
||||
Name: "reference",
|
||||
Type: "string",
|
||||
Format: TagNameRegexp.String(),
|
||||
Format: reference.TagRegexp.String(),
|
||||
Required: true,
|
||||
Description: `Tag or digest of the target manifest.`,
|
||||
}
|
||||
|
@ -389,7 +390,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
},
|
||||
{
|
||||
Name: RouteNameTags,
|
||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/tags/list",
|
||||
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list",
|
||||
Entity: "Tags",
|
||||
Description: "Retrieve information about tags.",
|
||||
Methods: []MethodDescriptor{
|
||||
|
@ -517,7 +518,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
},
|
||||
{
|
||||
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",
|
||||
Description: "Create, update and retrieve manifests.",
|
||||
Methods: []MethodDescriptor{
|
||||
|
@ -766,7 +767,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
|
||||
{
|
||||
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",
|
||||
Description: "Fetch the blob identified by `name` and `digest`. Used to fetch layers by digest.",
|
||||
Methods: []MethodDescriptor{
|
||||
|
@ -927,8 +928,8 @@ var routeDescriptors = []RouteDescriptor{
|
|||
|
||||
{
|
||||
Name: RouteNameBlobUpload,
|
||||
Path: "/v2/{name:" + RepositoryNameRegexp.String() + "}/blobs/uploads/",
|
||||
Entity: "Intiate Blob Upload",
|
||||
Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/",
|
||||
Entity: "Initiate Blob Upload",
|
||||
Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.",
|
||||
Methods: []MethodDescriptor{
|
||||
{
|
||||
|
@ -1041,7 +1042,7 @@ var routeDescriptors = []RouteDescriptor{
|
|||
|
||||
{
|
||||
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",
|
||||
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{
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
{
|
||||
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)
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/manifest"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if err := v2.ValidateRepositoryName(name); err != nil {
|
||||
if _, err := reference.ParseNamed(name); err != nil {
|
||||
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/context"
|
||||
"github.com/docker/distribution/digest"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/storage/cache"
|
||||
)
|
||||
|
||||
|
@ -26,7 +26,7 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
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/context"
|
||||
"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/garyburd/redigo/redis"
|
||||
)
|
||||
|
@ -41,7 +41,7 @@ func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorC
|
|||
|
||||
// RepositoryScoped returns the scoped cache.
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package storage
|
|||
import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/context"
|
||||
"github.com/docker/distribution/registry/api/v2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/storage/cache"
|
||||
storagedriver "github.com/docker/distribution/registry/storage/driver"
|
||||
)
|
||||
|
@ -64,10 +64,10 @@ func (reg *registry) Scope() distribution.Scope {
|
|||
// Repository returns an instance of the repository tied to the registry.
|
||||
// Instances should not be shared between goroutines but are cheap to
|
||||
// allocate. In general, they should be request scoped.
|
||||
func (reg *registry) Repository(ctx context.Context, name string) (distribution.Repository, error) {
|
||||
if err := v2.ValidateRepositoryName(name); err != nil {
|
||||
func (reg *registry) Repository(ctx context.Context, canonicalName string) (distribution.Repository, error) {
|
||||
if _, err := reference.ParseNamed(canonicalName); err != nil {
|
||||
return nil, distribution.ErrRepositoryNameInvalid{
|
||||
Name: name,
|
||||
Name: canonicalName,
|
||||
Reason: err,
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
|
|||
var descriptorCache distribution.BlobDescriptorService
|
||||
if reg.blobDescriptorCacheProvider != nil {
|
||||
var err error
|
||||
descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(name)
|
||||
descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(canonicalName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func (reg *registry) Repository(ctx context.Context, name string) (distribution.
|
|||
return &repository{
|
||||
ctx: ctx,
|
||||
registry: reg,
|
||||
name: name,
|
||||
name: canonicalName,
|
||||
descriptorCache: descriptorCache,
|
||||
}, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue