diff --git a/reference/reference.go b/reference/reference.go index abf4d70d..17e87d9b 100644 --- a/reference/reference.go +++ b/reference/reference.go @@ -7,11 +7,13 @@ // // // repository.go // repository := hostname ['/' component]+ -// hostname := component [':' port-number] +// hostname := hostcomponent [':' port-number] // component := alpha-numeric [separator alpha-numeric]* +// hostcomponent := [hostpart '.']* hostpart // alpha-numeric := /[a-zA-Z0-9]+/ -// separator := /[._-]/ +// separator := /[_-]/ // port-number := /[0-9]+/ +// hostpart := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // // // tag.go // tag := /[\w][\w.-]{0,127}/ @@ -20,167 +22,224 @@ // digest := digest-algorithm ":" digest-hex // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ] // digest-algorithm-separator := /[+.-_]/ -// digest-algorithm-component := /[A-Za-z]/ /[A-Za-z0-9]*/ -// digest-hex := /[A-Za-z0-9_-]+/ ; supports hex bytes or url safe base64 +// 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" - "regexp" + "fmt" "github.com/docker/distribution/digest" ) -// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference. -var ErrReferenceInvalidFormat = errors.New("invalid reference format") +const ( + // NameTotalLengthMax is the maximum total number of characters in a repository name. + NameTotalLengthMax = 255 +) -// Reference abstracts types that reference images in a certain way. +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 { - // Repository returns the repository part of a reference - Repository() Repository - // String returns the entire reference, including the repository part + // String returns the full reference String() string } -func parseHostname(s string) (hostname, tail string) { - tail = s - i := regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String()).FindStringIndex(s) - if i == nil { - return - } - return s[:i[1]], s[i[1]:] +// Named is an object with a full name +type Named interface { + Name() string } -func parseRepositoryName(s string) (repo, tail string) { - tail = s - i := regexp.MustCompile(`^/(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()).FindStringIndex(s) - if i == nil { - return - } - return s[:i[1]], s[i[1]:] +// Tagged is an object which has a tag +type Tagged interface { + Tag() string } -func parseTag(s string) (tag Tag, tail string) { - tail = s - if len(s) == 0 || s[0] != ':' { - return - } - tag, err := NewTag(s[1:]) - if err != nil { - return - } - tail = s[len(tag)+1:] - return +// Digested is an object which has a digest +// in which it can be referenced by +type Digested interface { + Digest() digest.Digest } -func parseDigest(s string) (dgst digest.Digest, tail string) { - tail = s - if len(s) == 0 || s[0] != '@' { - return +// Canonical reference is an object with a fully unique +// name including a name with hostname and digest +type Canonical interface { + Reference + Named + Digested +} + +// 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 } - dgst, err := digest.ParseDigest(s[1:]) - if err != nil { - return - } - tail = s[len(dgst)+1:] - return + 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) { - hostname, s := parseHostname(s) - name, s := parseRepositoryName(s) - repository := Repository{Hostname: hostname, Name: name} - if err := repository.Validate(); err != nil { - return nil, err - } - tag, s := parseTag(s) - dgst, s := parseDigest(s) - if len(s) > 0 { + matches := ReferenceRegexp.FindStringSubmatch(s) + if matches == nil { + if s == "" { + return nil, ErrNameEmpty + } + // TODO(dmcgowan): Provide more specific and helpful error return nil, ErrReferenceInvalidFormat } - if dgst != "" { - return DigestReference{repository: repository, digest: dgst, tag: tag}, nil + if len(matches[1]) > NameTotalLengthMax { + return nil, ErrNameTooLong } - if tag != "" { - return TagReference{repository: repository, tag: tag}, nil + + ref := reference{ + name: matches[1], + tag: matches[2], } - return nil, ErrReferenceInvalidFormat -} - -// DigestReference represents a reference of the form `repository@sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef`. -// Implements the Reference interface. -type DigestReference struct { - repository Repository - digest digest.Digest - tag Tag -} - -// Repository returns the repository part. -func (r DigestReference) Repository() Repository { return r.repository } - -// String returns the full string reference. -func (r DigestReference) String() string { - return r.repository.String() + "@" + string(r.digest) -} - -// NewDigestReference returns an initialized DigestReference. -func NewDigestReference(canonicalRepository string, digest digest.Digest, optionalTag Tag) (DigestReference, error) { - ref := DigestReference{} - - repo, err := NewRepository(canonicalRepository) - if err != nil { - return ref, err - } - ref.repository = repo - - if err := digest.Validate(); err != nil { - return ref, err - } - ref.digest = digest - - if len(optionalTag) > 0 { - if err := optionalTag.Validate(); err != nil { - return ref, err + if matches[3] != "" { + var err error + ref.digest, err = digest.ParseDigest(matches[3]) + if err != nil { + return nil, err } - ref.tag = optionalTag } - return ref, err -} - -// TagReference represents a reference of the form `repository:tag`. -// Implements the Reference interface. -type TagReference struct { - repository Repository - tag Tag -} - -// Repository returns the repository part. -func (r TagReference) Repository() Repository { return r.repository } - -// String returns the full string reference. -func (r TagReference) String() string { - return r.repository.String() + ":" + string(r.tag) -} - -// NewTagReference returns an initialized TagReference. -func NewTagReference(canonicalRepository string, tagName string) (TagReference, error) { - ref := TagReference{} - - repo, err := NewRepository(canonicalRepository) - if err != nil { - return ref, err + r := getBestReferenceType(ref) + if r == nil { + return nil, ErrNameEmpty } - ref.repository = repo - tag, err := NewTag(tagName) - if err != nil { - return ref, err - } - ref.tag = tag - - return ref, err + 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 } diff --git a/reference/reference_test.go b/reference/reference_test.go index 7af84f33..42d5a34b 100644 --- a/reference/reference_test.go +++ b/reference/reference_test.go @@ -1,56 +1,267 @@ package reference -/* -var refRegex = regexp.MustCompile(`^([a-z0-9]+(?:[-._][a-z0-9]+)*(?::[0-9]+(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+|(?:/[a-z0-9]+(?:[-._][a-z0-9]+)*)+)?)(:[\w][\w.-]{0,127})?(@` + digest.DigestRegexp.String() + `)?$`) +import ( + "strconv" + "strings" + "testing" -func getRepo(s string) string { - matches := refRegex.FindStringSubmatch(s) - if len(matches) == 0 { - return "" + "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", + }, } - return matches[1] -} + for _, testcase := range referenceTestcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.input)+": "+format, v...) + t.Fail() + } -func testRepository(prefix string) error { - for _, s := range []string{ - prefix + `@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, - prefix + `:frozen@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`, - prefix + `:latest`, - prefix, - } { - expected := getRepo(s) - ref, err := Parse(s) - if err != nil { - if expected == "" { - continue + 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) } - return err + continue + } else if err != nil { + failf("unexpected parse error: %v", err) + continue } - if repo := ref.Repository(); repo.String() != expected { - return fmt.Errorf("repository string: expected %q, got: %q", expected, repo) + 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") } - if refStr := ref.String(); refStr != s { - return fmt.Errorf("reference string: expected %q, got: %q", s, refStr) - } - } - return nil -} -func TestSimpleRepository(t *testing.T) { - if err := testRepository(`busybox`); err != nil { - t.Fatal(err) } } -func TestUrlRepository(t *testing.T) { - if err := testRepository(`docker.io/library/busybox`); err != nil { - t.Fatal(err) +func 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() + } -func TestPort(t *testing.T) { - if err := testRepository(`busybox:1234`); err != nil { - t.Fatal(err) + 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) + } } } -*/ diff --git a/reference/regexp.go b/reference/regexp.go new file mode 100644 index 00000000..aa3480c5 --- /dev/null +++ b/reference/regexp.go @@ -0,0 +1,37 @@ +package reference + +import "regexp" + +var ( + // 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, dash or underscore. + nameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`) + + nameRegexp = regexp.MustCompile(`(?:` + nameComponentRegexp.String() + `/)*` + nameComponentRegexp.String()) + + hostnameComponentRegexp = regexp.MustCompile(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-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() + `)$`) +) diff --git a/reference/regexp_test.go b/reference/regexp_test.go new file mode 100644 index 00000000..9435ee2a --- /dev/null +++ b/reference/regexp_test.go @@ -0,0 +1,398 @@ +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", matches) + } + if len(matches) < (len(m.subs) + 1) { + t.Errorf("Expected %d sub matches, only have %d", len(m.subs), len(matches)-1) + } + for i := range m.subs { + if m.subs[i] != matches[i+1] { + t.Errorf("Unexpected submatch %d: %q, expected %q", i+1, matches[i+1], m.subs[i]) + } + } + } 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"}, + }, + } + 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]) + } + +} diff --git a/reference/repository.go b/reference/repository.go deleted file mode 100644 index 936b0929..00000000 --- a/reference/repository.go +++ /dev/null @@ -1,136 +0,0 @@ -package reference - -import ( - "errors" - "fmt" - "regexp" - "strings" -) - -const ( - // RepositoryNameTotalLengthMax is the maximum total number of characters in a repository name. - RepositoryNameTotalLengthMax = 255 -) - -// RepositoryNameComponentRegexp restricts registry path component names to -// start with at least one letter or number, with following parts able to -// be separated by one period, dash or underscore. -var RepositoryNameComponentRegexp = regexp.MustCompile(`[a-zA-Z0-9]+(?:[._-][a-z0-9]+)*`) - -// RepositoryNameComponentAnchoredRegexp is the version of -// RepositoryNameComponentRegexp which must completely match the content -var RepositoryNameComponentAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameComponentRegexp.String() + `$`) - -// RepositoryNameHostnameRegexp restricts the registry hostname component of a repository name to -// start with a component as defined by RepositoryNameComponentRegexp and followed by an optional port. -var RepositoryNameHostnameRegexp = regexp.MustCompile(RepositoryNameComponentRegexp.String() + `(?::[0-9]+)?`) - -// RepositoryNameHostnameAnchoredRegexp is the version of -// RepositoryNameHostnameRegexp which must completely match the content. -var RepositoryNameHostnameAnchoredRegexp = regexp.MustCompile(`^` + RepositoryNameHostnameRegexp.String() + `$`) - -// RepositoryNameRegexp builds on RepositoryNameComponentRegexp to allow -// multiple path components, separated by a forward slash. -var RepositoryNameRegexp = regexp.MustCompile(`(?:` + RepositoryNameHostnameRegexp.String() + `/)?(?:` + RepositoryNameComponentRegexp.String() + `/)*` + RepositoryNameComponentRegexp.String()) - -var ( - // ErrRepositoryNameEmpty is returned for empty, invalid repository names. - ErrRepositoryNameEmpty = errors.New("repository name must have at least one component") - - // ErrRepositoryNameMissingHostname is returned when a repository name - // does not start with a hostname - ErrRepositoryNameMissingHostname = errors.New("repository name must start with a hostname") - - // ErrRepositoryNameHostnameInvalid is returned when a repository name - // does not match RepositoryNameHostnameRegexp - ErrRepositoryNameHostnameInvalid = fmt.Errorf("repository name must match %q", RepositoryNameHostnameRegexp.String()) - - // ErrRepositoryNameLong is returned when a repository name is longer than - // RepositoryNameTotalLengthMax - ErrRepositoryNameLong = fmt.Errorf("repository name must not be more than %v characters", RepositoryNameTotalLengthMax) - - // ErrRepositoryNameComponentInvalid is returned when a repository name does - // not match RepositoryNameComponentRegexp - ErrRepositoryNameComponentInvalid = fmt.Errorf("repository name component must match %q", RepositoryNameComponentRegexp.String()) -) - -// Repository represents a reference to a Repository. -type Repository struct { - // Hostname refers to the registry hostname where the repository resides. - Hostname string - // Name is a slash (`/`) separated list of string components. - Name string -} - -// String returns the string representation of a repository. -func (r Repository) String() string { - // Hostname is not supposed to be empty, but let's be nice. - if len(r.Hostname) == 0 { - return r.Name - } - return r.Hostname + "/" + r.Name -} - -// Validate ensures the repository name is valid for use in the -// registry. This function accepts a superset of what might be accepted by -// docker core or docker hub. If the name does not pass validation, an error, -// describing the conditions, is returned. -// -// Effectively, the name should comply with the following grammar: -// -// repository := hostname ['/' component]+ -// hostname := component [':' port-number] -// component := alpha-numeric [separator alpha-numeric]* -// alpha-numeric := /[a-zA-Z0-9]+/ -// separator := /[._-]/ -// port-number := /[0-9]+/ -// -// The result of the production should be limited to 255 characters. -func (r Repository) Validate() error { - n := len(r.String()) - switch { - case n == 0: - return ErrRepositoryNameEmpty - case n > RepositoryNameTotalLengthMax: - return ErrRepositoryNameLong - case len(r.Hostname) <= 0: - return ErrRepositoryNameMissingHostname - case !RepositoryNameHostnameAnchoredRegexp.MatchString(r.Hostname): - return ErrRepositoryNameHostnameInvalid - } - - components := r.Name - for { - var component string - sep := strings.Index(components, "/") - if sep >= 0 { - component = components[:sep] - components = components[sep+1:] - } else { // if no more slashes - component = components - components = "" - } - if !RepositoryNameComponentAnchoredRegexp.MatchString(component) { - return ErrRepositoryNameComponentInvalid - } - if sep < 0 { - return nil - } - } -} - -// NewRepository returns a valid Repository from an input string representing -// the canonical form of a repository name. -// If the validation fails, an error is returned. -func NewRepository(canonicalName string) (repo Repository, err error) { - if len(canonicalName) == 0 { - return repo, ErrRepositoryNameEmpty - } - i := strings.Index(canonicalName, "/") - if i <= 0 { - return repo, ErrRepositoryNameMissingHostname - } - repo.Hostname = canonicalName[:i] - repo.Name = canonicalName[i+1:] - return repo, repo.Validate() -} diff --git a/reference/repository_test.go b/reference/repository_test.go deleted file mode 100644 index 67d65f9d..00000000 --- a/reference/repository_test.go +++ /dev/null @@ -1,286 +0,0 @@ -package reference - -import ( - "regexp" - "strconv" - "strings" - "testing" -) - -var ( - // regexpTestcases is a unified set of testcases for - // TestValidateRepositoryName and TestRepositoryNameRegexp. - // Some of them are valid inputs for one and not the other. - regexpTestcases = []struct { - // input is the repository name or name component testcase - input string - // err is the error expected from ValidateRepositoryName, or nil - err error - // invalid should be true if the testcase is *not* expected to - // match RepositoryNameRegexp - invalid bool - }{ - { - input: "", - err: ErrRepositoryNameEmpty, - invalid: true, - }, - { - input: "short", - err: ErrRepositoryNameMissingHostname, - }, - { - input: "simple/name", - }, - { - input: "library/ubuntu", - }, - { - input: "docker/stevvooe/app", - }, - { - input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", - }, - { - input: "aa/aa/bb/bb/bb", - }, - { - input: "a/a/a/b/b", - }, - { - input: "a/a/a/a/", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "a//a/a", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "a", - err: ErrRepositoryNameMissingHostname, - }, - { - input: "a/aa", - }, - { - input: "aa/a", - }, - { - input: "a/aa/a", - }, - { - input: "foo.com/", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo.com:8080/bar", - }, - { - input: "foo.com/bar", - }, - { - input: "foo.com/bar/baz", - }, - { - input: "foo.com/bar/baz/quux", - }, - { - input: "blog.foo.com/bar/baz", - }, - { - input: "asdf", - err: ErrRepositoryNameMissingHostname, - }, - { - input: "aa/asdf$$^/aa", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "asdf$$^/aa", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "aa-a/aa", - }, - { - input: "aa/aa", - }, - { - input: "a-a/a-a", - }, - { - input: "a", - err: ErrRepositoryNameMissingHostname, - }, - { - input: "a/image", - }, - { - input: "a-/a/a/a", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "a/a-/a/a/a", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - // total length = 255 - input: "a/" + strings.Repeat("a", 253), - }, - { - // total length = 256 - input: "b/" + strings.Repeat("a", 254), - err: ErrRepositoryNameLong, - }, - { - input: "-foo/bar", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "foo/bar-", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "foo-/bar", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "foo/-bar", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "_foo/bar", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "foo/bar_", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "____/____", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "_docker/_docker", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "docker_/docker_", - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "do__cker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "docker./docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: ".docker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "do..cker/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "docker-/docker", - err: ErrRepositoryNameComponentInvalid, - invalid: true, - }, - { - input: "-docker/docker", - err: ErrRepositoryNameComponentInvalid, - }, - { - input: "xn--n3h.com/myimage", // http://☃.com in punycode - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "xn--7o8h.com/myimage", // http://🐳.com in punycode - err: ErrRepositoryNameHostnameInvalid, - invalid: true, - }, - { - input: "b.gcr.io/test.example.com/my-app", // embedded domain component - }, - { - input: "xn--n3h.com/myimage", // http://☃.com in punycode - }, - { - input: "xn--7o8h.com/myimage", // http://🐳.com in punycode - }, - { - input: "registry.io/foo/project--id.module--name.ver---sion--name", // image with hostname - }, - } -) - -// TestValidateRepositoryName tests the ValidateRepositoryName function, -// which uses RepositoryNameComponentAnchoredRegexp for validation -func TestValidateRepositoryName(t *testing.T) { - for _, testcase := range regexpTestcases { - failf := func(format string, v ...interface{}) { - t.Logf(strconv.Quote(testcase.input)+": "+format, v...) - t.Fail() - } - - if _, err := NewRepository(testcase.input); err != testcase.err { - if testcase.err != nil { - if err != nil { - failf("unexpected error for invalid repository: got %v, expected %v", err, testcase.err) - } else { - failf("expected invalid repository: %v", testcase.err) - } - } else { - if err != nil { - // Wrong error returned. - failf("unexpected error validating repository name: %v, expected %v", err, testcase.err) - } else { - failf("unexpected error validating repository name: %v", err) - } - } - } - } -} - -func TestRepositoryNameRegexp(t *testing.T) { - AnchoredRepositoryNameRegexp := regexp.MustCompile(`^` + RepositoryNameRegexp.String() + `$`) - for _, testcase := range regexpTestcases { - failf := func(format string, v ...interface{}) { - t.Logf(strconv.Quote(testcase.input)+": "+format, v...) - t.Fail() - } - - matches := AnchoredRepositoryNameRegexp.MatchString(testcase.input) - if matches == testcase.invalid { - if testcase.invalid { - failf("expected invalid repository name %s", testcase.input) - } else { - failf("expected valid repository name %s", testcase.input) - } - } - } -} diff --git a/reference/tag.go b/reference/tag.go deleted file mode 100644 index 8deee36e..00000000 --- a/reference/tag.go +++ /dev/null @@ -1,38 +0,0 @@ -package reference - -import ( - "fmt" - "regexp" -) - -var ( - // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. - TagRegexp = regexp.MustCompile(`[\w][\w.-]{0,127}`) - - // TagAnchoredRegexp matches valid tag names, anchored at the start and - // end of the matched string. - TagAnchoredRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) - - // ErrTagInvalid is returned when a tag does not match TagAnchoredRegexp. - ErrTagInvalid = fmt.Errorf("tag name must match %q", TagRegexp.String()) -) - -// Tag represents an image's tag name. -type Tag string - -// NewTag returns a valid Tag from an input string s. -// If the validation fails, an error is returned. -func NewTag(s string) (Tag, error) { - tag := Tag(s) - return tag, tag.Validate() -} - -// Validate returns ErrTagInvalid if tag does not match TagAnchoredRegexp. -// -// tag := [\w][\w.-]{0,127} -func (tag Tag) Validate() error { - if !TagAnchoredRegexp.MatchString(string(tag)) { - return ErrTagInvalid - } - return nil -} diff --git a/registry/api/v2/descriptors.go b/registry/api/v2/descriptors.go index ef37997a..9cfb2fb5 100644 --- a/registry/api/v2/descriptors.go +++ b/registry/api/v2/descriptors.go @@ -13,7 +13,7 @@ var ( nameParameterDescriptor = ParameterDescriptor{ Name: "name", Type: "string", - Format: reference.RepositoryNameRegexp.String(), + Format: reference.NameRegexp.String(), Required: true, Description: `Name of the target repository.`, } @@ -390,7 +390,7 @@ var routeDescriptors = []RouteDescriptor{ }, { Name: RouteNameTags, - Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/tags/list", + Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list", Entity: "Tags", Description: "Retrieve information about tags.", Methods: []MethodDescriptor{ @@ -518,7 +518,7 @@ var routeDescriptors = []RouteDescriptor{ }, { Name: RouteNameManifest, - Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", + Path: "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", Entity: "Manifest", Description: "Create, update, delete and retrieve manifests.", Methods: []MethodDescriptor{ @@ -783,7 +783,7 @@ var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBlob, - Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", + Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", Entity: "Blob", Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", Methods: []MethodDescriptor{ @@ -1007,7 +1007,7 @@ var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBlobUpload, - Path: "/v2/{name:" + reference.RepositoryNameRegexp.String() + "}/blobs/uploads/", + 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{ @@ -1129,7 +1129,7 @@ var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBlobUploadChunk, - Path: "/v2/{name:" + reference.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{ diff --git a/registry/api/v2/routes_test.go b/registry/api/v2/routes_test.go index b8d724df..f6379977 100644 --- a/registry/api/v2/routes_test.go +++ b/registry/api/v2/routes_test.go @@ -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) diff --git a/registry/client/repository.go b/registry/client/repository.go index db45a464..fc709ded 100644 --- a/registry/client/repository.go +++ b/registry/client/repository.go @@ -97,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 canonical repository name and base URL. -func NewRepository(ctx context.Context, canonicalName, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { - if _, err := reference.NewRepository(canonicalName); err != nil { +// 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 := reference.ParseNamed(name); err != nil { return nil, err } @@ -116,7 +116,7 @@ func NewRepository(ctx context.Context, canonicalName, baseURL string, transport return &repository{ client: client, ub: ub, - name: canonicalName, + name: name, context: ctx, }, nil } diff --git a/registry/storage/cache/memory/memory.go b/registry/storage/cache/memory/memory.go index 725a68e7..68a68f08 100644 --- a/registry/storage/cache/memory/memory.go +++ b/registry/storage/cache/memory/memory.go @@ -25,8 +25,8 @@ func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider } } -func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) { - if _, err := reference.NewRepository(canonicalName); err != nil { +func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { + if _, err := reference.ParseNamed(repo); err != nil { return nil, err } @@ -34,9 +34,9 @@ func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(canonicalNam defer imbdcp.mu.RUnlock() return &repositoryScopedInMemoryBlobDescriptorCache{ - repo: canonicalName, + repo: repo, parent: imbdcp, - repository: imbdcp.repositories[canonicalName], + repository: imbdcp.repositories[repo], }, nil } diff --git a/registry/storage/cache/redis/redis.go b/registry/storage/cache/redis/redis.go index 54138f3d..1736756e 100644 --- a/registry/storage/cache/redis/redis.go +++ b/registry/storage/cache/redis/redis.go @@ -40,13 +40,13 @@ func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorC } // RepositoryScoped returns the scoped cache. -func (rbds *redisBlobDescriptorService) RepositoryScoped(canonicalName string) (distribution.BlobDescriptorService, error) { - if _, err := reference.NewRepository(canonicalName); err != nil { +func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { + if _, err := reference.ParseNamed(repo); err != nil { return nil, err } return &repositoryScopedRedisBlobDescriptorService{ - repo: canonicalName, + repo: repo, upstream: rbds, }, nil } diff --git a/registry/storage/registry.go b/registry/storage/registry.go index e3b132c5..1050920a 100644 --- a/registry/storage/registry.go +++ b/registry/storage/registry.go @@ -108,7 +108,7 @@ func (reg *registry) Scope() distribution.Scope { // 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, canonicalName string) (distribution.Repository, error) { - if _, err := reference.NewRepository(canonicalName); err != nil { + if _, err := reference.ParseNamed(canonicalName); err != nil { return nil, distribution.ErrRepositoryNameInvalid{ Name: canonicalName, Reason: err,