replace reference/docker for github.com/distribution/reference v0.5.0

The reference/docker package was a fork of github.com/distribution/distribution,
which could not easily be used as a direct dependency, as it brought many other
dependencies with it.

The "reference' package has now moved to a separate repository, which means
we can replace the local fork, and use the upstream implementation again.

The new module was extracted from the distribution repository at commit:
b9b19409cf

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2023-08-30 22:17:58 +02:00
parent f9608c0027
commit 4923470902
44 changed files with 1241 additions and 2438 deletions

View File

@@ -16,43 +16,35 @@
package docker
import "path"
import "github.com/distribution/reference"
// IsNameOnly returns true if reference only contains a repo name.
func IsNameOnly(ref Named) bool {
if _, ok := ref.(NamedTagged); ok {
return false
}
if _, ok := ref.(Canonical); ok {
return false
}
return true
//
// Deprecated: use [reference.IsNameOnly].
func IsNameOnly(ref reference.Named) bool {
return reference.IsNameOnly(ref)
}
// FamiliarName returns the familiar name string
// for the given named, familiarizing if needed.
func FamiliarName(ref Named) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().Name()
}
return ref.Name()
//
// Deprecated: use [reference.FamiliarName].
func FamiliarName(ref reference.Named) string {
return reference.FamiliarName(ref)
}
// FamiliarString returns the familiar string representation
// for the given reference, familiarizing if needed.
func FamiliarString(ref Reference) string {
if nn, ok := ref.(normalizedNamed); ok {
return nn.Familiar().String()
}
return ref.String()
//
// Deprecated: use [reference.FamiliarString].
func FamiliarString(ref reference.Reference) string {
return reference.FamiliarString(ref)
}
// FamiliarMatch reports whether ref matches the specified pattern.
// See https://godoc.org/path#Match for supported patterns.
func FamiliarMatch(pattern string, ref Reference) (bool, error) {
matched, err := path.Match(pattern, FamiliarString(ref))
if namedRef, isNamed := ref.(Named); isNamed && !matched {
matched, _ = path.Match(pattern, FamiliarName(namedRef))
}
return matched, err
// See [path.Match] for supported patterns.
//
// Deprecated: use [reference.FamiliarMatch].
func FamiliarMatch(pattern string, ref reference.Reference) (bool, error) {
return reference.FamiliarMatch(pattern, ref)
}

View File

@@ -1,196 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"fmt"
"strings"
"github.com/opencontainers/go-digest"
)
var (
legacyDefaultDomain = "index.docker.io"
defaultDomain = "docker.io"
officialRepoName = "library"
defaultTag = "latest"
)
// normalizedNamed represents a name which has been
// normalized and has a familiar form. A familiar name
// is what is used in Docker UI. An example normalized
// name is "docker.io/library/ubuntu" and corresponding
// familiar name of "ubuntu".
type normalizedNamed interface {
Named
Familiar() Named
}
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
func ParseNormalizedNamed(s string) (Named, error) {
if ok := anchoredIdentifierRegexp.MatchString(s); ok {
return nil, fmt.Errorf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings", s)
}
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, fmt.Errorf("invalid reference format: repository name (%s) must be lowercase", remoteName)
}
ref, err := Parse(domain + "/" + remainder)
if err != nil {
return nil, err
}
named, isNamed := ref.(Named)
if !isNamed {
return nil, fmt.Errorf("reference %s has no name", ref.String())
}
return named, nil
}
// ParseDockerRef normalizes the image reference following the docker convention. This is added
// mainly for backward compatibility.
// The reference returned can only be either tagged or digested. For reference contains both tag
// and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
// sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
// docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
func ParseDockerRef(ref string) (Named, error) {
named, err := ParseNormalizedNamed(ref)
if err != nil {
return nil, err
}
if _, ok := named.(NamedTagged); ok {
if canonical, ok := named.(Canonical); ok {
// The reference is both tagged and digested, only
// return digested.
newNamed, err := WithName(canonical.Name())
if err != nil {
return nil, err
}
newCanonical, err := WithDigest(newNamed, canonical.Digest())
if err != nil {
return nil, err
}
return newCanonical, nil
}
}
return TagNameOnly(named), nil
}
// splitDockerDomain splits a repository name to domain and remotename string.
// If no valid domain is found, the default domain is used. Repository name
// needs to be already validated before.
func splitDockerDomain(name string) (domain, remainder string) {
i := strings.IndexRune(name, '/')
if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) {
domain, remainder = defaultDomain, name
} else {
domain, remainder = name[:i], name[i+1:]
}
if domain == legacyDefaultDomain {
domain = defaultDomain
}
if domain == defaultDomain && !strings.ContainsRune(remainder, '/') {
remainder = officialRepoName + "/" + remainder
}
return
}
// familiarizeName returns a shortened version of the name familiar
// to the Docker UI. Familiar names have the default domain
// "docker.io" and "library/" repository prefix removed.
// For example, "docker.io/library/redis" will have the familiar
// name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
// Returns a familiarized named only reference.
func familiarizeName(named namedRepository) repository {
repo := repository{
domain: named.Domain(),
path: named.Path(),
}
if repo.domain == defaultDomain {
repo.domain = ""
// Handle official repositories which have the pattern "library/<official repo name>"
if split := strings.Split(repo.path, "/"); len(split) == 2 && split[0] == officialRepoName {
repo.path = split[1]
}
}
return repo
}
func (r reference) Familiar() Named {
return reference{
namedRepository: familiarizeName(r.namedRepository),
tag: r.tag,
digest: r.digest,
}
}
func (r repository) Familiar() Named {
return familiarizeName(r)
}
func (t taggedReference) Familiar() Named {
return taggedReference{
namedRepository: familiarizeName(t.namedRepository),
tag: t.tag,
}
}
func (c canonicalReference) Familiar() Named {
return canonicalReference{
namedRepository: familiarizeName(c.namedRepository),
digest: c.digest,
}
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
func TagNameOnly(ref Named) Named {
if IsNameOnly(ref) {
namedTagged, err := WithTag(ref, defaultTag)
if err != nil {
// Default tag must be valid, to create a NamedTagged
// type with non-validated input the WithTag function
// should be used instead
panic(err)
}
return namedTagged
}
return ref
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
func ParseAnyReference(ref string) (Reference, error) {
if ok := anchoredIdentifierRegexp.MatchString(ref); ok {
return digestReference("sha256:" + ref), nil
}
if dgst, err := digest.Parse(ref); err == nil {
return digestReference(dgst), nil
}
return ParseNormalizedNamed(ref)
}

View File

@@ -0,0 +1,55 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"github.com/distribution/reference"
)
// ParseNormalizedNamed parses a string into a named reference
// transforming a familiar name from Docker UI to a fully
// qualified reference. If the value may be an identifier
// use ParseAnyReference.
//
// Deprecated: use [reference.ParseNormalizedNamed].
func ParseNormalizedNamed(s string) (reference.Named, error) {
return reference.ParseNormalizedNamed(s)
}
// ParseDockerRef normalizes the image reference following the docker convention,
// which allows for references to contain both a tag and a digest.
//
// Deprecated: use [reference.ParseDockerRef].
func ParseDockerRef(ref string) (reference.Named, error) {
return reference.ParseDockerRef(ref)
}
// TagNameOnly adds the default tag "latest" to a reference if it only has
// a repo name.
//
// Deprecated: use [reference.TagNameOnly].
func TagNameOnly(ref reference.Named) reference.Named {
return reference.TagNameOnly(ref)
}
// ParseAnyReference parses a reference string as a possible identifier,
// full digest, or familiar name.
//
// Deprecated: use [reference.ParseAnyReference].
func ParseAnyReference(ref string) (reference.Reference, error) {
return reference.ParseAnyReference(ref)
}

View File

@@ -1,701 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"strconv"
"testing"
"github.com/opencontainers/go-digest"
)
func TestValidateReferenceName(t *testing.T) {
validRepoNames := []string{
"docker/docker",
"library/debian",
"debian",
"docker.io/docker/docker",
"docker.io/library/debian",
"docker.io/debian",
"index.docker.io/docker/docker",
"index.docker.io/library/debian",
"index.docker.io/debian",
"127.0.0.1:5000/docker/docker",
"127.0.0.1:5000/library/debian",
"127.0.0.1:5000/debian",
"192.168.0.1",
"192.168.0.1:80",
"192.168.0.1:8/debian",
"192.168.0.2:25000/debian",
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
"[fc00::1]:5000/docker",
"[fc00::1]:5000/docker/docker",
"[fc00:1:2:3:4:5:6:7]:5000/library/debian",
// This test case was moved from invalid to valid since it is valid input
// when specified with a hostname, it removes the ambiguity from about
// whether the value is an identifier or repository name
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
"Docker/docker",
"DOCKER/docker",
}
invalidRepoNames := []string{
"https://github.com/docker/docker",
"docker/Docker",
"-docker",
"-docker/docker",
"-docker.io/docker/docker",
"docker///docker",
"docker.io/docker/Docker",
"docker.io/docker///docker",
"[fc00::1]",
"[fc00::1]:5000",
"fc00::1:5000/debian",
"[fe80::1%eth0]:5000/debian",
"[2001:db8:3:4::192.0.2.33]:5000/debian",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
}
for _, name := range invalidRepoNames {
_, err := ParseNormalizedNamed(name)
if err == nil {
t.Fatalf("Expected invalid repo name for %q", name)
}
}
for _, name := range validRepoNames {
_, err := ParseNormalizedNamed(name)
if err != nil {
t.Fatalf("Error parsing repo name %s, got: %q", name, err)
}
}
}
func TestValidateRemoteName(t *testing.T) {
validRepositoryNames := []string{
// Sanity check.
"docker/docker",
// Allow 64-character non-hexadecimal names (hexadecimal names are forbidden).
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
// Allow embedded hyphens.
"docker-rules/docker",
// Allow multiple hyphens as well.
"docker---rules/docker",
// Username doc and image name docker being tested.
"doc/docker",
// single character names are now allowed.
"d/docker",
"jess/t",
// Consecutive underscores.
"dock__er/docker",
}
for _, repositoryName := range validRepositoryNames {
_, err := ParseNormalizedNamed(repositoryName)
if err != nil {
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
}
}
invalidRepositoryNames := []string{
// Disallow capital letters.
"docker/Docker",
// Only allow one slash.
"docker///docker",
// Disallow 64-character hexadecimal.
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// Disallow leading and trailing hyphens in namespace.
"-docker/docker",
"docker-/docker",
"-docker-/docker",
// Don't allow underscores everywhere (as opposed to hyphens).
"____/____",
"_docker/_docker",
// Disallow consecutive periods.
"dock..er/docker",
"dock_.er/docker",
"dock-.er/docker",
// No repository.
"docker/",
// namespace too long
"this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255_this_is_not_a_valid_namespace_because_its_lenth_is_greater_than_255/docker",
}
for _, repositoryName := range invalidRepositoryNames {
if _, err := ParseNormalizedNamed(repositoryName); err == nil {
t.Errorf("Repository name should be invalid: %v", repositoryName)
}
}
}
func TestParseRepositoryInfo(t *testing.T) {
type tcase struct {
RemoteName, FamiliarName, FullName, AmbiguousName, Domain string
}
tcases := []tcase{
{
RemoteName: "fooo/bar",
FamiliarName: "fooo/bar",
FullName: "docker.io/fooo/bar",
AmbiguousName: "index.docker.io/fooo/bar",
Domain: "docker.io",
},
{
RemoteName: "library/ubuntu",
FamiliarName: "ubuntu",
FullName: "docker.io/library/ubuntu",
AmbiguousName: "library/ubuntu",
Domain: "docker.io",
},
{
RemoteName: "nonlibrary/ubuntu",
FamiliarName: "nonlibrary/ubuntu",
FullName: "docker.io/nonlibrary/ubuntu",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "other/library",
FamiliarName: "other/library",
FullName: "docker.io/other/library",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "private/moonbase",
FamiliarName: "127.0.0.1:8000/private/moonbase",
FullName: "127.0.0.1:8000/private/moonbase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "privatebase",
FamiliarName: "127.0.0.1:8000/privatebase",
FullName: "127.0.0.1:8000/privatebase",
AmbiguousName: "",
Domain: "127.0.0.1:8000",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com/private/moonbase",
FullName: "example.com/private/moonbase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "privatebase",
FamiliarName: "example.com/privatebase",
FullName: "example.com/privatebase",
AmbiguousName: "",
Domain: "example.com",
},
{
RemoteName: "private/moonbase",
FamiliarName: "example.com:8000/private/moonbase",
FullName: "example.com:8000/private/moonbase",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "privatebasee",
FamiliarName: "example.com:8000/privatebasee",
FullName: "example.com:8000/privatebasee",
AmbiguousName: "",
Domain: "example.com:8000",
},
{
RemoteName: "library/ubuntu-12.04-base",
FamiliarName: "ubuntu-12.04-base",
FullName: "docker.io/library/ubuntu-12.04-base",
AmbiguousName: "index.docker.io/library/ubuntu-12.04-base",
Domain: "docker.io",
},
{
RemoteName: "library/foo",
FamiliarName: "foo",
FullName: "docker.io/library/foo",
AmbiguousName: "docker.io/foo",
Domain: "docker.io",
},
{
RemoteName: "library/foo/bar",
FamiliarName: "library/foo/bar",
FullName: "docker.io/library/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "store/foo/bar",
FamiliarName: "store/foo/bar",
FullName: "docker.io/store/foo/bar",
AmbiguousName: "",
Domain: "docker.io",
},
{
RemoteName: "bar",
FamiliarName: "Foo/bar",
FullName: "Foo/bar",
AmbiguousName: "",
Domain: "Foo",
},
{
RemoteName: "bar",
FamiliarName: "FOO/bar",
FullName: "FOO/bar",
AmbiguousName: "",
Domain: "FOO",
},
}
for _, tcase := range tcases {
refStrings := []string{tcase.FamiliarName, tcase.FullName}
if tcase.AmbiguousName != "" {
refStrings = append(refStrings, tcase.AmbiguousName)
}
var refs []Named
for _, r := range refStrings {
named, err := ParseNormalizedNamed(r)
if err != nil {
t.Fatal(err)
}
refs = append(refs, named)
}
for _, r := range refs {
if expected, actual := tcase.FamiliarName, FamiliarName(r); expected != actual {
t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.FullName, r.String(); expected != actual {
t.Fatalf("Invalid canonical reference for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.Domain, Domain(r); expected != actual {
t.Fatalf("Invalid domain for %q. Expected %q, got %q", r, expected, actual)
}
if expected, actual := tcase.RemoteName, Path(r); expected != actual {
t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual)
}
}
}
}
func TestParseReferenceWithTagAndDigest(t *testing.T) {
shortRef := "busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"
ref, err := ParseNormalizedNamed(shortRef)
if err != nil {
t.Fatal(err)
}
if expected, actual := "docker.io/library/"+shortRef, ref.String(); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
if _, isTagged := ref.(NamedTagged); !isTagged {
t.Fatalf("Reference from %q should support tag", ref)
}
if _, isCanonical := ref.(Canonical); !isCanonical {
t.Fatalf("Reference from %q should support digest", ref)
}
if expected, actual := shortRef, FamiliarString(ref); actual != expected {
t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual)
}
}
func TestInvalidReferenceComponents(t *testing.T) {
if _, err := ParseNormalizedNamed("-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid name")
}
ref, err := ParseNormalizedNamed("busybox")
if err != nil {
t.Fatal(err)
}
if _, err := WithTag(ref, "-foo"); err == nil {
t.Fatal("Expected WithName to detect invalid tag")
}
if _, err := WithDigest(ref, digest.Digest("foo")); err == nil {
t.Fatal("Expected WithDigest to detect invalid digest")
}
}
func equalReference(r1, r2 Reference) bool {
switch v1 := r1.(type) {
case digestReference:
if v2, ok := r2.(digestReference); ok {
return v1 == v2
}
case repository:
if v2, ok := r2.(repository); ok {
return v1 == v2
}
case taggedReference:
if v2, ok := r2.(taggedReference); ok {
return v1 == v2
}
case canonicalReference:
if v2, ok := r2.(canonicalReference); ok {
return v1 == v2
}
case reference:
if v2, ok := r2.(reference); ok {
return v1 == v2
}
}
return false
}
func TestParseAnyReference(t *testing.T) {
tcases := []struct {
Reference string
Equivalent string
Expected Reference
}{
{
Reference: "redis",
Equivalent: "docker.io/library/redis",
},
{
Reference: "redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "docker.io/library/redis:latest",
Equivalent: "docker.io/library/redis:latest",
},
{
Reference: "redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/library/redis@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dmcgowan/myapp",
Equivalent: "docker.io/dmcgowan/myapp",
},
{
Reference: "dmcgowan/myapp:latest",
Equivalent: "docker.io/dmcgowan/myapp:latest",
},
{
Reference: "docker.io/mcgowan/myapp:latest",
Equivalent: "docker.io/mcgowan/myapp:latest",
},
{
Reference: "dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Equivalent: "docker.io/dmcgowan/myapp@sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
Expected: digestReference("sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c"),
Equivalent: "sha256:dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9c",
},
{
Reference: "dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
Equivalent: "docker.io/library/dbcc1c35ac38df41fd2f5e4130b32ffdb93ebae8b3dbe638c23575912276fc9",
},
{
Reference: "dbcc1",
Equivalent: "docker.io/library/dbcc1",
},
}
for _, tcase := range tcases {
var ref Reference
var err error
ref, err = ParseAnyReference(tcase.Reference)
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Reference, err)
}
if ref.String() != tcase.Equivalent {
t.Fatalf("Unexpected string: %s, expected %s", ref.String(), tcase.Equivalent)
}
expected := tcase.Expected
if expected == nil {
expected, err = Parse(tcase.Equivalent)
if err != nil {
t.Fatalf("Error parsing reference %s: %v", tcase.Equivalent, err)
}
}
if !equalReference(ref, expected) {
t.Errorf("Unexpected reference %#v, expected %#v", ref, expected)
}
}
}
func TestNormalizedSplitHostname(t *testing.T) {
testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "docker.io",
name: "test_com/foo",
},
{
input: "docker/migrator",
domain: "docker.io",
name: "docker/migrator",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "xn--n3h.com/foo",
domain: "xn--n3h.com",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
{
input: "docker.io/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
{
input: "docker.io/library/foo/bar",
domain: "docker.io",
name: "library/foo/bar",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := ParseNormalizedNamed(testcase.input)
if err != nil {
failf("error parsing name: %s", err)
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
}
}
func TestMatchError(t *testing.T) {
named, err := ParseAnyReference("foo")
if err != nil {
t.Fatal(err)
}
_, err = FamiliarMatch("[-x]", named)
if err == nil {
t.Fatalf("expected an error, got nothing")
}
}
func TestMatch(t *testing.T) {
matchCases := []struct {
reference string
pattern string
expected bool
}{
{
reference: "foo",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/any/bat",
pattern: "foo/**/ba[rz]",
expected: false,
},
{
reference: "foo/a/bar",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/b/baz",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/**/ba[rz]",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/*/baz:tag",
expected: true,
},
{
reference: "foo/c/baz:tag",
pattern: "foo/c/baz:tag",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "*/foo/c/baz",
expected: true,
},
{
reference: "example.com/foo/c/baz:tag",
pattern: "example.com/foo/c/baz",
expected: true,
},
}
for _, c := range matchCases {
named, err := ParseAnyReference(c.reference)
if err != nil {
t.Fatal(err)
}
actual, err := FamiliarMatch(c.pattern, named)
if err != nil {
t.Fatal(err)
}
if actual != c.expected {
t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual)
}
}
}
func TestParseDockerRef(t *testing.T) {
testcases := []struct {
name string
input string
expected string
}{
{
name: "nothing",
input: "busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "tag only",
input: "busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "digest only",
input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
expected: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
},
{
name: "path only",
input: "library/busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "hostname only",
input: "docker.io/busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "no tag",
input: "docker.io/library/busybox",
expected: "docker.io/library/busybox:latest",
},
{
name: "no path",
input: "docker.io/busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "no hostname",
input: "library/busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "full reference with tag",
input: "docker.io/library/busybox:latest",
expected: "docker.io/library/busybox:latest",
},
{
name: "gcr reference without tag",
input: "gcr.io/library/busybox",
expected: "gcr.io/library/busybox:latest",
},
{
name: "both tag and digest",
input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
expected: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582",
},
}
for _, test := range testcases {
t.Run(test.name, func(t *testing.T) {
normalized, err := ParseDockerRef(test.input)
if err != nil {
t.Fatal(err)
}
output := normalized.String()
if output != test.expected {
t.Fatalf("expected %q to be parsed as %v, got %v", test.input, test.expected, output)
}
_, err = Parse(output)
if err != nil {
t.Fatalf("%q should be a valid reference, but got an error: %v", output, err)
}
})
}
}

View File

@@ -1,453 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package docker provides a general type to represent any way of referencing images within the registry.
// Its main purpose is to abstract tags and digests (content-addressable hash).
//
// Grammar
//
// reference := name [ ":" tag ] [ "@" digest ]
// name := [domain '/'] path-component ['/' path-component]*
// domain := host [':' port-number]
// host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A
// domain-name := domain-component ['.' domain-component]*
// domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
// port-number := /[0-9]+/
// path-component := alpha-numeric [separator alpha-numeric]*
// alpha-numeric := /[a-z0-9]+/
// separator := /[_.]|__|[-]*/
//
// tag := /[\w][\w.-]{0,127}/
//
// digest := digest-algorithm ":" digest-hex
// digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
// digest-algorithm-separator := /[+.-_]/
// digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
// digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
//
// identifier := /[a-f0-9]{64}/
// short-identifier := /[a-f0-9]{6,64}/
package docker
import (
"errors"
"fmt"
"strings"
"github.com/opencontainers/go-digest"
)
const (
// NameTotalLengthMax is the maximum total number of characters in a repository name.
NameTotalLengthMax = 255
)
var (
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
ErrReferenceInvalidFormat = errors.New("invalid reference format")
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
ErrTagInvalidFormat = errors.New("invalid tag format")
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
ErrDigestInvalidFormat = errors.New("invalid digest format")
// ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
ErrNameContainsUppercase = errors.New("repository name must be lowercase")
// ErrNameEmpty is returned for empty, invalid repository names.
ErrNameEmpty = errors.New("repository name must have at least one component")
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
// ErrNameNotCanonical is returned when a name is not canonical.
ErrNameNotCanonical = errors.New("repository name must be canonical")
)
// Reference is an opaque object reference identifier that may include
// modifiers such as a hostname, name, tag, and digest.
type Reference interface {
// String returns the full reference
String() string
}
// Field provides a wrapper type for resolving correct reference types when
// working with encoding.
type Field struct {
reference Reference
}
// AsField wraps a reference in a Field for encoding.
func AsField(reference Reference) Field {
return Field{reference}
}
// Reference unwraps the reference type from the field to
// return the Reference object. This object should be
// of the appropriate type to further check for different
// reference types.
func (f Field) Reference() Reference {
return f.reference
}
// MarshalText serializes the field to byte text which
// is the string of the reference.
func (f Field) MarshalText() (p []byte, err error) {
return []byte(f.reference.String()), nil
}
// UnmarshalText parses text bytes by invoking the
// reference parser to ensure the appropriately
// typed reference object is wrapped by field.
func (f *Field) UnmarshalText(p []byte) error {
r, err := Parse(string(p))
if err != nil {
return err
}
f.reference = r
return nil
}
// Named is an object with a full name
type Named interface {
Reference
Name() string
}
// Tagged is an object which has a tag
type Tagged interface {
Reference
Tag() string
}
// NamedTagged is an object including a name and tag.
type NamedTagged interface {
Named
Tag() string
}
// Digested is an object which has a digest
// in which it can be referenced by
type Digested interface {
Reference
Digest() digest.Digest
}
// Canonical reference is an object with a fully unique
// name including a name with domain and digest
type Canonical interface {
Named
Digest() digest.Digest
}
// namedRepository is a reference to a repository with a name.
// A namedRepository has both domain and path components.
type namedRepository interface {
Named
Domain() string
Path() string
}
// Domain returns the domain part of the Named reference
func Domain(named Named) string {
if r, ok := named.(namedRepository); ok {
return r.Domain()
}
domain, _ := splitDomain(named.Name())
return domain
}
// Path returns the name without the domain part of the Named reference
func Path(named Named) (name string) {
if r, ok := named.(namedRepository); ok {
return r.Path()
}
_, path := splitDomain(named.Name())
return path
}
func splitDomain(name string) (string, string) {
match := anchoredNameRegexp.FindStringSubmatch(name)
if len(match) != 3 {
return "", name
}
return match[1], match[2]
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
// DEPRECATED: Use Domain or Path
func SplitHostname(named Named) (string, string) {
if r, ok := named.(namedRepository); ok {
return r.Domain(), r.Path()
}
return splitDomain(named.Name())
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: Parse will not handle short digests.
func Parse(s string) (Reference, error) {
matches := ReferenceRegexp.FindStringSubmatch(s)
if matches == nil {
if s == "" {
return nil, ErrNameEmpty
}
if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
return nil, ErrNameContainsUppercase
}
return nil, ErrReferenceInvalidFormat
}
if len(matches[1]) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
var repo repository
nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
if len(nameMatch) == 3 {
repo.domain = nameMatch[1]
repo.path = nameMatch[2]
} else {
repo.domain = ""
repo.path = matches[1]
}
ref := reference{
namedRepository: repo,
tag: matches[2],
}
if matches[3] != "" {
var err error
ref.digest, err = digest.Parse(matches[3])
if err != nil {
return nil, err
}
}
r := getBestReferenceType(ref)
if r == nil {
return nil, ErrNameEmpty
}
return r, nil
}
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name and be in the canonical
// form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: ParseNamed will not handle short digests.
func ParseNamed(s string) (Named, error) {
named, err := ParseNormalizedNamed(s)
if err != nil {
return nil, err
}
if named.String() != s {
return nil, ErrNameNotCanonical
}
return named, nil
}
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
func WithName(name string) (Named, error) {
if len(name) > NameTotalLengthMax {
return nil, ErrNameTooLong
}
match := anchoredNameRegexp.FindStringSubmatch(name)
if match == nil || len(match) != 3 {
return nil, ErrReferenceInvalidFormat
}
return repository{
domain: match[1],
path: match[2],
}, nil
}
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) {
if !anchoredTagRegexp.MatchString(tag) {
return nil, ErrTagInvalidFormat
}
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if canonical, ok := name.(Canonical); ok {
return reference{
namedRepository: repo,
tag: tag,
digest: canonical.Digest(),
}, nil
}
return taggedReference{
namedRepository: repo,
tag: tag,
}, nil
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
if !anchoredDigestRegexp.MatchString(digest.String()) {
return nil, ErrDigestInvalidFormat
}
var repo repository
if r, ok := name.(namedRepository); ok {
repo.domain = r.Domain()
repo.path = r.Path()
} else {
repo.path = name.Name()
}
if tagged, ok := name.(Tagged); ok {
return reference{
namedRepository: repo,
tag: tagged.Tag(),
digest: digest,
}, nil
}
return canonicalReference{
namedRepository: repo,
digest: digest,
}, nil
}
// TrimNamed removes any tag or digest from the named reference.
func TrimNamed(ref Named) Named {
repo := repository{}
if r, ok := ref.(namedRepository); ok {
repo.domain, repo.path = r.Domain(), r.Path()
} else {
repo.domain, repo.path = splitDomain(ref.Name())
}
return repo
}
func getBestReferenceType(ref reference) Reference {
if ref.Name() == "" {
// Allow digest only references
if ref.digest != "" {
return digestReference(ref.digest)
}
return nil
}
if ref.tag == "" {
if ref.digest != "" {
return canonicalReference{
namedRepository: ref.namedRepository,
digest: ref.digest,
}
}
return ref.namedRepository
}
if ref.digest == "" {
return taggedReference{
namedRepository: ref.namedRepository,
tag: ref.tag,
}
}
return ref
}
type reference struct {
namedRepository
tag string
digest digest.Digest
}
func (r reference) String() string {
return r.Name() + ":" + r.tag + "@" + r.digest.String()
}
func (r reference) Tag() string {
return r.tag
}
func (r reference) Digest() digest.Digest {
return r.digest
}
type repository struct {
domain string
path string
}
func (r repository) String() string {
return r.Name()
}
func (r repository) Name() string {
if r.domain == "" {
return r.path
}
return r.domain + "/" + r.path
}
func (r repository) Domain() string {
return r.domain
}
func (r repository) Path() string {
return r.path
}
type digestReference digest.Digest
func (d digestReference) String() string {
return digest.Digest(d).String()
}
func (d digestReference) Digest() digest.Digest {
return digest.Digest(d)
}
type taggedReference struct {
namedRepository
tag string
}
func (t taggedReference) String() string {
return t.Name() + ":" + t.tag
}
func (t taggedReference) Tag() string {
return t.tag
}
type canonicalReference struct {
namedRepository
digest digest.Digest
}
func (c canonicalReference) String() string {
return c.Name() + "@" + c.digest.String()
}
func (c canonicalReference) Digest() digest.Digest {
return c.digest
}

View File

@@ -0,0 +1,188 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package docker is deprecated, and has moved to github.com/distribution/reference.
//
// Deprecated: use github.com/distribution/reference instead.
package docker
import (
"github.com/distribution/reference"
"github.com/opencontainers/go-digest"
)
const (
// NameTotalLengthMax is the maximum total number of characters in a repository name.
//
// Deprecated: use [reference.NameTotalLengthMax].
NameTotalLengthMax = reference.NameTotalLengthMax
)
var (
// ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
//
// Deprecated: use [reference.ErrReferenceInvalidFormat].
ErrReferenceInvalidFormat = reference.ErrReferenceInvalidFormat
// ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
//
// Deprecated: use [reference.ErrTagInvalidFormat].
ErrTagInvalidFormat = reference.ErrTagInvalidFormat
// ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
//
// Deprecated: use [reference.ErrDigestInvalidFormat].
ErrDigestInvalidFormat = reference.ErrDigestInvalidFormat
// ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
//
// Deprecated: use [reference.ErrNameContainsUppercase].
ErrNameContainsUppercase = reference.ErrNameContainsUppercase
// ErrNameEmpty is returned for empty, invalid repository names.
//
// Deprecated: use [reference.ErrNameEmpty].
ErrNameEmpty = reference.ErrNameEmpty
// ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
//
// Deprecated: use [reference.ErrNameTooLong].
ErrNameTooLong = reference.ErrNameTooLong
// ErrNameNotCanonical is returned when a name is not canonical.
//
// Deprecated: use [reference.ErrNameNotCanonical].
ErrNameNotCanonical = reference.ErrNameNotCanonical
)
// Reference is an opaque object reference identifier that may include
// modifiers such as a hostname, name, tag, and digest.
//
// Deprecated: use [reference.Reference].
type Reference = reference.Reference
// Field provides a wrapper type for resolving correct reference types when
// working with encoding.
//
// Deprecated: use [reference.Field].
type Field = reference.Field
// AsField wraps a reference in a Field for encoding.
//
// Deprecated: use [reference.AsField].
func AsField(ref reference.Reference) reference.Field {
return reference.AsField(ref)
}
// Named is an object with a full name
//
// Deprecated: use [reference.Named].
type Named = reference.Named
// Tagged is an object which has a tag
//
// Deprecated: use [reference.Tagged].
type Tagged = reference.Tagged
// NamedTagged is an object including a name and tag.
//
// Deprecated: use [reference.NamedTagged].
type NamedTagged reference.NamedTagged
// Digested is an object which has a digest
// in which it can be referenced by
//
// Deprecated: use [reference.Digested].
type Digested reference.Digested
// Canonical reference is an object with a fully unique
// name including a name with domain and digest
//
// Deprecated: use [reference.Canonical].
type Canonical reference.Canonical
// Domain returns the domain part of the [Named] reference.
//
// Deprecated: use [reference.Domain].
func Domain(named reference.Named) string {
return reference.Domain(named)
}
// Path returns the name without the domain part of the [Named] reference.
//
// Deprecated: use [reference.Path].
func Path(named reference.Named) (name string) {
return reference.Path(named)
}
// SplitHostname splits a named reference into a
// hostname and name string. If no valid hostname is
// found, the hostname is empty and the full value
// is returned as name
//
// Deprecated: Use [reference.Domain] or [reference.Path].
func SplitHostname(named reference.Named) (string, string) {
return reference.SplitHostname(named)
}
// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
//
// Deprecated: use [reference.Parse].
func Parse(s string) (reference.Reference, error) {
return reference.Parse(s)
}
// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name and be in the canonical
// form, otherwise an error is returned.
// If an error was encountered it is returned, along with a nil Reference.
//
// Deprecated: use [reference.ParseNamed].
func ParseNamed(s string) (reference.Named, error) {
return reference.ParseNamed(s)
}
// WithName returns a named object representing the given string. If the input
// is invalid ErrReferenceInvalidFormat will be returned.
//
// Deprecated: use [reference.WithName].
func WithName(name string) (reference.Named, error) {
return reference.WithName(name)
}
// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
//
// Deprecated: use [reference.WithTag].
func WithTag(name reference.Named, tag string) (reference.NamedTagged, error) {
return reference.WithTag(name, tag)
}
// WithDigest combines the name from "name" and the digest from "digest" to form
// a reference incorporating both the name and the digest.
//
// Deprecated: use [reference.WithDigest].
func WithDigest(name reference.Named, digest digest.Digest) (reference.Canonical, error) {
return reference.WithDigest(name, digest)
}
// TrimNamed removes any tag or digest from the named reference.
//
// Deprecated: use [reference.TrimNamed].
func TrimNamed(ref reference.Named) reference.Named {
return reference.TrimNamed(ref)
}

View File

@@ -1,770 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
_ "crypto/sha256"
_ "crypto/sha512"
"encoding/json"
"strconv"
"strings"
"testing"
"github.com/opencontainers/go-digest"
)
func TestReferenceParse(t *testing.T) {
// referenceTestcases is a unified set of testcases for
// testing the parsing of references
referenceTestcases := []struct {
// input is the repository name or name component testcase
input string
// err is the error expected from Parse, or nil
err error
// repository is the string representation for the reference
repository string
// domain is the domain expected in the reference
domain string
// tag is the tag for the reference
tag string
// digest is the digest for the reference (enforces digest reference)
digest string
}{
{
input: "test_com",
repository: "test_com",
},
{
input: "test.com:tag",
repository: "test.com",
tag: "tag",
},
{
input: "test.com:5000",
repository: "test.com",
tag: "5000",
},
{
input: "test.com/repo:tag",
domain: "test.com",
repository: "test.com/repo",
tag: "tag",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "test:5000/repo:tag",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
},
{
input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "test:5000",
repository: "test:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "test:5000/repo",
domain: "test:5000",
repository: "test:5000/repo",
},
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "repo@sha256:ffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestInvalidLength,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: digest.ErrDigestUnsupported,
},
{
input: "Uppercase:tag",
err: ErrNameContainsUppercase,
},
// FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes.
// See https://github.com/distribution/distribution/pull/1778, and https://github.com/docker/docker/pull/20175
// {
// input: "Uppercase/lowercase:tag",
// err: ErrNameContainsUppercase,
// },
{
input: "test:5000/Uppercase/lowercase:tag",
err: ErrNameContainsUppercase,
},
{
input: "lowercase:Uppercase",
repository: "lowercase",
tag: "Uppercase",
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max",
domain: "a",
repository: strings.Repeat("a/", 127) + "a",
tag: "tag-puts-this-over-max",
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
},
{
input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag",
domain: "sub-dom1.foo.com",
repository: "sub-dom1.foo.com/bar/baz/quux",
tag: "some-long-tag",
},
{
input: "b.gcr.io/test.example.com/my-app:test.example.com",
domain: "b.gcr.io",
repository: "b.gcr.io/test.example.com/my-app",
tag: "test.example.com",
},
{
input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode
domain: "xn--n3h.com",
repository: "xn--n3h.com/myimage",
tag: "xn--n3h.com",
},
{
input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode
domain: "xn--7o8h.com",
repository: "xn--7o8h.com/myimage",
tag: "xn--7o8h.com",
digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "foo_bar.com:8080",
repository: "foo_bar.com",
tag: "8080",
},
{
input: "foo/foo_bar.com:8080",
domain: "foo",
repository: "foo/foo_bar.com",
tag: "8080",
},
{
input: "192.168.1.1",
repository: "192.168.1.1",
},
{
input: "192.168.1.1:tag",
repository: "192.168.1.1",
tag: "tag",
},
{
input: "192.168.1.1:5000",
repository: "192.168.1.1",
tag: "5000",
},
{
input: "192.168.1.1/repo",
domain: "192.168.1.1",
repository: "192.168.1.1/repo",
},
{
input: "192.168.1.1:5000/repo",
domain: "192.168.1.1:5000",
repository: "192.168.1.1:5000/repo",
},
{
input: "192.168.1.1:5000/repo:5050",
domain: "192.168.1.1:5000",
repository: "192.168.1.1:5000/repo",
tag: "5050",
},
{
input: "[2001:db8::1]",
err: ErrReferenceInvalidFormat,
},
{
input: "[2001:db8::1]:5000",
err: ErrReferenceInvalidFormat,
},
{
input: "[2001:db8::1]:tag",
err: ErrReferenceInvalidFormat,
},
{
input: "[2001:db8::1]/repo",
domain: "[2001:db8::1]",
repository: "[2001:db8::1]/repo",
},
{
input: "[2001:db8:1:2:3:4:5:6]/repo:tag",
domain: "[2001:db8:1:2:3:4:5:6]",
repository: "[2001:db8:1:2:3:4:5:6]/repo",
tag: "tag",
},
{
input: "[2001:db8::1]:5000/repo",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
},
{
input: "[2001:db8::1]:5000/repo:tag",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
tag: "tag",
},
{
input: "[2001:db8::1]:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "[2001:db8::1]:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
domain: "[2001:db8::1]:5000",
repository: "[2001:db8::1]:5000/repo",
tag: "tag",
digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
},
{
input: "[2001:db8::]:5000/repo",
domain: "[2001:db8::]:5000",
repository: "[2001:db8::]:5000/repo",
},
{
input: "[::1]:5000/repo",
domain: "[::1]:5000",
repository: "[::1]:5000/repo",
},
{
input: "[fe80::1%eth0]:5000/repo",
err: ErrReferenceInvalidFormat,
},
{
input: "[fe80::1%@invalidzone]:5000/repo",
err: ErrReferenceInvalidFormat,
},
}
for _, testcase := range referenceTestcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
repo, err := Parse(testcase.input)
if testcase.err != nil {
if err == nil {
failf("missing expected error: %v", testcase.err)
} else if testcase.err != err {
failf("mismatched error: got %v, expected %v", err, testcase.err)
}
continue
} else if err != nil {
failf("unexpected parse error: %v", err)
continue
}
if repo.String() != testcase.input {
failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input)
}
if named, ok := repo.(Named); ok {
if named.Name() != testcase.repository {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository)
}
domain, _ := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
} else if testcase.repository != "" || testcase.domain != "" {
failf("expected named type, got %T", repo)
}
tagged, ok := repo.(Tagged)
if testcase.tag != "" {
if ok {
if tagged.Tag() != testcase.tag {
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
}
} else {
failf("expected tagged type, got %T", repo)
}
} else if ok {
failf("unexpected tagged type")
}
digested, ok := repo.(Digested)
if testcase.digest != "" {
if ok {
if digested.Digest().String() != testcase.digest {
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
}
} else {
failf("expected digested type, got %T", repo)
}
} else if ok {
failf("unexpected digested type")
}
}
}
// TestWithNameFailure tests cases where WithName should fail. Cases where it
// should succeed are covered by TestSplitHostname, below.
func TestWithNameFailure(t *testing.T) {
testcases := []struct {
input string
err error
}{
{
input: "",
err: ErrNameEmpty,
},
{
input: ":justtag",
err: ErrReferenceInvalidFormat,
},
{
input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
err: ErrReferenceInvalidFormat,
},
{
input: strings.Repeat("a/", 128) + "a:tag",
err: ErrNameTooLong,
},
{
input: "aa/asdf$$^/aa",
err: ErrReferenceInvalidFormat,
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
_, err := WithName(testcase.input)
if err == nil {
failf("no error parsing name. expected: %s", testcase.err)
}
}
}
func TestSplitHostname(t *testing.T) {
testcases := []struct {
input string
domain string
name string
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test_com/foo",
domain: "",
name: "test_com/foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test.com:8080/foo",
domain: "test.com:8080",
name: "foo",
},
{
input: "test-com:8080/foo",
domain: "test-com:8080",
name: "foo",
},
{
input: "xn--n3h.com:18080/foo",
domain: "xn--n3h.com:18080",
name: "foo",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.input)
if err != nil {
failf("error parsing name: %s", err)
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
}
}
type serializationType struct {
Description string
Field Field
}
func TestSerialization(t *testing.T) {
testcases := []struct {
description string
input string
name string
tag string
digest string
err error
}{
{
description: "empty value",
err: ErrNameEmpty,
},
{
description: "just a name",
input: "example.com:8000/named",
name: "example.com:8000/named",
},
{
description: "name with a tag",
input: "example.com:8000/named:tagged",
name: "example.com:8000/named",
tag: "tagged",
},
{
description: "name with digest",
input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112",
name: "other.com/named",
digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
m := map[string]string{
"Description": testcase.description,
"Field": testcase.input,
}
b, err := json.Marshal(m)
if err != nil {
failf("error marshalling: %v", err)
}
t := serializationType{}
if err := json.Unmarshal(b, &t); err != nil {
if testcase.err == nil {
failf("error unmarshalling: %v", err)
}
if err != testcase.err {
failf("wrong error, expected %v, got %v", testcase.err, err)
}
continue
} else if testcase.err != nil {
failf("expected error unmarshalling: %v", testcase.err)
}
if t.Description != testcase.description {
failf("wrong description, expected %q, got %q", testcase.description, t.Description)
}
ref := t.Field.Reference()
if named, ok := ref.(Named); ok {
if named.Name() != testcase.name {
failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name)
}
} else if testcase.name != "" {
failf("expected named type, got %T", ref)
}
tagged, ok := ref.(Tagged)
if testcase.tag != "" {
if ok {
if tagged.Tag() != testcase.tag {
failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag)
}
} else {
failf("expected tagged type, got %T", ref)
}
} else if ok {
failf("unexpected tagged type")
}
digested, ok := ref.(Digested)
if testcase.digest != "" {
if ok {
if digested.Digest().String() != testcase.digest {
failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest)
}
} else {
failf("expected digested type, got %T", ref)
}
} else if ok {
failf("unexpected digested type")
}
t = serializationType{
Description: testcase.description,
Field: AsField(ref),
}
b2, err := json.Marshal(t)
if err != nil {
failf("error marshing serialization type: %v", err)
}
if string(b) != string(b2) {
failf("unexpected serialized value: expected %q, got %q", string(b), string(b2))
}
// Ensure t.Field is not implementing "Reference" directly, getting
// around the Reference type system
var fieldInterface interface{} = t.Field
if _, ok := fieldInterface.(Reference); ok {
failf("field should not implement Reference interface")
}
}
}
func TestWithTag(t *testing.T) {
testcases := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
tag: "tag",
combined: "test.com/foo:tag",
},
{
name: "foo",
tag: "tag2",
combined: "foo:tag2",
},
{
name: "test.com:8000/foo",
tag: "tag4",
combined: "test.com:8000/foo:tag4",
},
{
name: "test.com:8000/foo",
tag: "TAG5",
combined: "test.com:8000/foo:TAG5",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "TAG5",
combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.name)
if err != nil {
failf("error parsing name: %s", err)
}
if testcase.digest != "" {
canonical, err := WithDigest(named, testcase.digest)
if err != nil {
failf("error adding digest")
}
named = canonical
}
tagged, err := WithTag(named, testcase.tag)
if err != nil {
failf("WithTag failed: %s", err)
}
if tagged.String() != testcase.combined {
failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined)
}
}
}
func TestWithDigest(t *testing.T) {
testcases := []struct {
name string
digest digest.Digest
tag string
combined string
}{
{
name: "test.com/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765",
},
{
name: "test.com:8000/foo",
digest: "sha256:1234567890098765432112345667890098765",
tag: "latest",
combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765",
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.name)+": "+format, v...)
t.Fail()
}
named, err := WithName(testcase.name)
if err != nil {
failf("error parsing name: %s", err)
}
if testcase.tag != "" {
tagged, err := WithTag(named, testcase.tag)
if err != nil {
failf("error adding tag")
}
named = tagged
}
digested, err := WithDigest(named, testcase.digest)
if err != nil {
failf("WithDigest failed: %s", err)
}
if digested.String() != testcase.combined {
failf("unexpected: got %q, expected %q", digested.String(), testcase.combined)
}
}
}
func TestParseNamed(t *testing.T) {
testcases := []struct {
input string
domain string
name string
err error
}{
{
input: "test.com/foo",
domain: "test.com",
name: "foo",
},
{
input: "test:8080/foo",
domain: "test:8080",
name: "foo",
},
{
input: "test_com/foo",
err: ErrNameNotCanonical,
},
{
input: "test.com",
err: ErrNameNotCanonical,
},
{
input: "foo",
err: ErrNameNotCanonical,
},
{
input: "library/foo",
err: ErrNameNotCanonical,
},
{
input: "docker.io/library/foo",
domain: "docker.io",
name: "library/foo",
},
// Ambiguous case, parser will add "library/" to foo
{
input: "docker.io/foo",
err: ErrNameNotCanonical,
},
}
for _, testcase := range testcases {
failf := func(format string, v ...interface{}) {
t.Logf(strconv.Quote(testcase.input)+": "+format, v...)
t.Fail()
}
named, err := ParseNamed(testcase.input)
if err != nil && testcase.err == nil {
failf("error parsing name: %s", err)
continue
} else if err == nil && testcase.err != nil {
failf("parsing succeeded: expected error %v", testcase.err)
continue
} else if err != testcase.err {
failf("unexpected error %v, expected %v", err, testcase.err)
continue
} else if err != nil {
continue
}
domain, name := SplitHostname(named)
if domain != testcase.domain {
failf("unexpected domain: got %q, expected %q", domain, testcase.domain)
}
if name != testcase.name {
failf("unexpected name: got %q, expected %q", name, testcase.name)
}
}
}

View File

@@ -1,191 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import "regexp"
var (
// alphaNumeric defines the alpha numeric atom, typically a
// component of names. This only allows lower case characters and digits.
alphaNumeric = `[a-z0-9]+`
// separator defines the separators allowed to be embedded in name
// components. This allow one period, one or two underscore and multiple
// dashes. Repeated dashes and underscores are intentionally treated
// differently. In order to support valid hostnames as name components,
// supporting repeated dash was added. Additionally double underscore is
// now allowed as a separator to loosen the restriction for previously
// supported names.
separator = `(?:[._]|__|[-]*)`
// nameComponent restricts registry path component names to start
// with at least one letter or number, with following parts able to be
// separated by one period, one or two underscore and multiple dashes.
nameComponent = expression(
alphaNumeric,
optional(repeated(separator, alphaNumeric)))
// domainNameComponent restricts the registry domain component of a
// repository name to start with a component as defined by DomainRegexp.
domainNameComponent = `(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`
// ipv6address are enclosed between square brackets and may be represented
// in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
// are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
// IPv4-Mapped are deliberately excluded.
ipv6address = expression(
literal(`[`), `(?:[a-fA-F0-9:]+)`, literal(`]`),
)
// domainName defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names. This includes IPv4 addresses on decimal format.
domainName = expression(
domainNameComponent,
optional(repeated(literal(`.`), domainNameComponent)),
)
// host defines the structure of potential domains based on the URI
// Host subcomponent on rfc3986. It may be a subset of DNS domain name,
// or an IPv4 address in decimal format, or an IPv6 address between square
// brackets (excluding zone identifiers as defined by rfc6874 or special
// addresses such as IPv4-Mapped).
host = `(?:` + domainName + `|` + ipv6address + `)`
// allowed by the URI Host subcomponent on rfc3986 to ensure backwards
// compatibility with Docker image names.
domain = expression(
host,
optional(literal(`:`), `[0-9]+`))
// DomainRegexp defines the structure of potential domain components
// that may be part of image names. This is purposely a subset of what is
// allowed by DNS to ensure backwards compatibility with Docker image
// names.
DomainRegexp = regexp.MustCompile(domain)
tag = `[\w][\w.-]{0,127}`
// TagRegexp matches valid tag names. From docker/docker:graph/tags.go.
TagRegexp = regexp.MustCompile(tag)
anchoredTag = anchored(tag)
// anchoredTagRegexp matches valid tag names, anchored at the start and
// end of the matched string.
anchoredTagRegexp = regexp.MustCompile(anchoredTag)
digestPat = `[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`
// DigestRegexp matches valid digests.
DigestRegexp = regexp.MustCompile(digestPat)
anchoredDigest = anchored(digestPat)
// anchoredDigestRegexp matches valid digests, anchored at the start and
// end of the matched string.
anchoredDigestRegexp = regexp.MustCompile(anchoredDigest)
namePat = expression(
optional(domain, literal(`/`)),
nameComponent,
optional(repeated(literal(`/`), nameComponent)))
// NameRegexp is the format for the name component of references. The
// regexp has capturing groups for the domain and name part omitting
// the separating forward slash from either.
NameRegexp = regexp.MustCompile(namePat)
anchoredName = anchored(
optional(capture(domain), literal(`/`)),
capture(nameComponent,
optional(repeated(literal(`/`), nameComponent))))
// anchoredNameRegexp is used to parse a name value, capturing the
// domain and trailing components.
anchoredNameRegexp = regexp.MustCompile(anchoredName)
referencePat = anchored(capture(namePat),
optional(literal(":"), capture(tag)),
optional(literal("@"), capture(digestPat)))
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
ReferenceRegexp = regexp.MustCompile(referencePat)
identifier = `([a-f0-9]{64})`
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
IdentifierRegexp = regexp.MustCompile(identifier)
shortIdentifier = `([a-f0-9]{6,64})`
// ShortIdentifierRegexp is the format used to represent a prefix
// of an identifier. A prefix may be used to match a sha256 identifier
// within a list of trusted identifiers.
ShortIdentifierRegexp = regexp.MustCompile(shortIdentifier)
anchoredIdentifier = anchored(identifier)
// anchoredIdentifierRegexp is used to check or match an
// identifier value, anchored at start and end of string.
anchoredIdentifierRegexp = regexp.MustCompile(anchoredIdentifier)
)
// literal compiles s into a literal regular expression, escaping any regexp
// reserved characters.
func literal(s string) string {
re := regexp.MustCompile(regexp.QuoteMeta(s))
if _, complete := re.LiteralPrefix(); !complete {
panic("must be a literal")
}
return re.String()
}
// expression defines a full expression, where each regular expression must
// follow the previous.
func expression(res ...string) string {
var s string
for _, re := range res {
s += re
}
return s
}
// optional wraps the expression in a non-capturing group and makes the
// production optional.
func optional(res ...string) string {
return group(expression(res...)) + `?`
}
// repeated wraps the regexp in a non-capturing group to get one or more
// matches.
func repeated(res ...string) string {
return group(expression(res...)) + `+`
}
// group wraps the regexp in a non-capturing group.
func group(res ...string) string {
return `(?:` + expression(res...) + `)`
}
// capture wraps the expression in a capturing group.
func capture(res ...string) string {
return `(` + expression(res...) + `)`
}
// anchored anchors the regular expression by adding start and end delimiters.
func anchored(res ...string) string {
return `^` + expression(res...) + `$`
}

View File

@@ -0,0 +1,66 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"github.com/distribution/reference"
)
// DigestRegexp matches well-formed digests, including algorithm (e.g. "sha256:<encoded>").
//
// Deprecated: use [reference.DigestRegexp].
var DigestRegexp = reference.DigestRegexp
// DomainRegexp matches hostname or IP-addresses, optionally including a port
// number. It defines the structure of potential domain components that may be
// part of image names. This is purposely a subset of what is allowed by DNS to
// ensure backwards compatibility with Docker image names. It may be a subset of
// DNS domain name, an IPv4 address in decimal format, or an IPv6 address between
// square brackets (excluding zone identifiers as defined by [RFC 6874] or special
// addresses such as IPv4-Mapped).
//
// Deprecated: use [reference.DomainRegexp].
//
// [RFC 6874]: https://www.rfc-editor.org/rfc/rfc6874.
var DomainRegexp = reference.DigestRegexp
// IdentifierRegexp is the format for string identifier used as a
// content addressable identifier using sha256. These identifiers
// are like digests without the algorithm, since sha256 is used.
//
// Deprecated: use [reference.IdentifierRegexp].
var IdentifierRegexp = reference.IdentifierRegexp
// NameRegexp is the format for the name component of references, including
// an optional domain and port, but without tag or digest suffix.
//
// Deprecated: use [reference.NameRegexp].
var NameRegexp = reference.NameRegexp
// ReferenceRegexp is the full supported format of a reference. The regexp
// is anchored and has capturing groups for name, tag, and digest
// components.
//
// Deprecated: use [reference.ReferenceRegexp].
var ReferenceRegexp = reference.ReferenceRegexp
// TagRegexp matches valid tag names. From [docker/docker:graph/tags.go].
//
// Deprecated: use [reference.TagRegexp].
//
// [docker/docker:graph/tags.go]: https://github.com/moby/moby/blob/v1.6.0/graph/tags.go#L26-L28
var TagRegexp = reference.TagRegexp

View File

@@ -1,574 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"regexp"
"strings"
"testing"
)
type regexpMatch struct {
input string
match bool
subs []string
}
func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) {
matches := r.FindStringSubmatch(m.input)
if m.match && matches != nil {
if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input {
t.Fatalf("Bad match result %#v for %q", matches, m.input)
}
if len(matches) < (len(m.subs) + 1) {
t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input)
}
for i := range m.subs {
if m.subs[i] != matches[i+1] {
t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input)
}
}
} else if m.match {
t.Errorf("Expected match for %q", m.input)
} else if matches != nil {
t.Errorf("Unexpected match for %q", m.input)
}
}
func TestDomainRegexp(t *testing.T) {
hostcases := []regexpMatch{
{
input: "test.com",
match: true,
},
{
input: "test.com:10304",
match: true,
},
{
input: "test.com:http",
match: false,
},
{
input: "localhost",
match: true,
},
{
input: "localhost:8080",
match: true,
},
{
input: "a",
match: true,
},
{
input: "a.b",
match: true,
},
{
input: "ab.cd.com",
match: true,
},
{
input: "a-b.com",
match: true,
},
{
input: "-ab.com",
match: false,
},
{
input: "ab-.com",
match: false,
},
{
input: "ab.c-om",
match: true,
},
{
input: "ab.-com",
match: false,
},
{
input: "ab.com-",
match: false,
},
{
input: "0101.com",
match: true, // TODO(dmcgowan): valid if this should be allowed
},
{
input: "001a.com",
match: true,
},
{
input: "b.gbc.io:443",
match: true,
},
{
input: "b.gbc.io",
match: true,
},
{
input: "xn--n3h.com", // ☃.com in punycode
match: true,
},
{
input: "Asdf.com", // uppercase character
match: true,
},
{
input: "192.168.1.1:75050", // ipv4
match: true,
},
{
input: "192.168.1.1:750050", // port with more than 5 digits, it will fail on validation
match: true,
},
{
input: "[fd00:1:2::3]:75050", // ipv6 compressed
match: true,
},
{
input: "[fd00:1:2::3]75050", // ipv6 wrong port separator
match: false,
},
{
input: "[fd00:1:2::3]::75050", // ipv6 wrong port separator
match: false,
},
{
input: "[fd00:1:2::3%eth0]:75050", // ipv6 with zone
match: false,
},
{
input: "[fd00123123123]:75050", // ipv6 wrong format, will fail in validation
match: true,
},
{
input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:75050", // ipv6 long format
match: true,
},
{
input: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:750505", // ipv6 long format and invalid port, it will fail in validation
match: true,
},
{
input: "fd00:1:2::3:75050", // bad ipv6 without square brackets
match: false,
},
}
r := regexp.MustCompile(`^` + DomainRegexp.String() + `$`)
for i := range hostcases {
checkRegexp(t, r, hostcases[i])
}
}
func TestFullNameRegexp(t *testing.T) {
if anchoredNameRegexp.NumSubexp() != 2 {
t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2",
anchoredNameRegexp, anchoredNameRegexp.NumSubexp())
}
testcases := []regexpMatch{
{
input: "",
match: false,
},
{
input: "short",
match: true,
subs: []string{"", "short"},
},
{
input: "simple/name",
match: true,
subs: []string{"simple", "name"},
},
{
input: "library/ubuntu",
match: true,
subs: []string{"library", "ubuntu"},
},
{
input: "docker/stevvooe/app",
match: true,
subs: []string{"docker", "stevvooe/app"},
},
{
input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"},
},
{
input: "aa/aa/bb/bb/bb",
match: true,
subs: []string{"aa", "aa/bb/bb/bb"},
},
{
input: "a/a/a/a",
match: true,
subs: []string{"a", "a/a/a"},
},
{
input: "a/a/a/a/",
match: false,
},
{
input: "a//a/a",
match: false,
},
{
input: "a",
match: true,
subs: []string{"", "a"},
},
{
input: "a/aa",
match: true,
subs: []string{"a", "aa"},
},
{
input: "a/aa/a",
match: true,
subs: []string{"a", "aa/a"},
},
{
input: "foo.com",
match: true,
subs: []string{"", "foo.com"},
},
{
input: "foo.com/",
match: false,
},
{
input: "foo.com:8080/bar",
match: true,
subs: []string{"foo.com:8080", "bar"},
},
{
input: "foo.com:http/bar",
match: false,
},
{
input: "foo.com/bar",
match: true,
subs: []string{"foo.com", "bar"},
},
{
input: "foo.com/bar/baz",
match: true,
subs: []string{"foo.com", "bar/baz"},
},
{
input: "localhost:8080/bar",
match: true,
subs: []string{"localhost:8080", "bar"},
},
{
input: "sub-dom1.foo.com/bar/baz/quux",
match: true,
subs: []string{"sub-dom1.foo.com", "bar/baz/quux"},
},
{
input: "blog.foo.com/bar/baz",
match: true,
subs: []string{"blog.foo.com", "bar/baz"},
},
{
input: "a^a",
match: false,
},
{
input: "aa/asdf$$^/aa",
match: false,
},
{
input: "asdf$$^/aa",
match: false,
},
{
input: "aa-a/a",
match: true,
subs: []string{"aa-a", "a"},
},
{
input: strings.Repeat("a/", 128) + "a",
match: true,
subs: []string{"a", strings.Repeat("a/", 127) + "a"},
},
{
input: "a-/a/a/a",
match: false,
},
{
input: "foo.com/a-/a/a",
match: false,
},
{
input: "-foo/bar",
match: false,
},
{
input: "foo/bar-",
match: false,
},
{
input: "foo-/bar",
match: false,
},
{
input: "foo/-bar",
match: false,
},
{
input: "_foo/bar",
match: false,
},
{
input: "foo_bar",
match: true,
subs: []string{"", "foo_bar"},
},
{
input: "foo_bar.com",
match: true,
subs: []string{"", "foo_bar.com"},
},
{
input: "foo_bar.com:8080",
match: false,
},
{
input: "foo_bar.com:8080/app",
match: false,
},
{
input: "foo.com/foo_bar",
match: true,
subs: []string{"foo.com", "foo_bar"},
},
{
input: "____/____",
match: false,
},
{
input: "_docker/_docker",
match: false,
},
{
input: "docker_/docker_",
match: false,
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
},
{
input: "xn--n3h.com/myimage", // ☃.com in punycode
match: true,
subs: []string{"xn--n3h.com", "myimage"},
},
{
input: "xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"xn--7o8h.com", "myimage"},
},
{
input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode
match: true,
subs: []string{"example.com", "xn--7o8h.com/myimage"},
},
{
input: "example.com/some_separator__underscore/myimage",
match: true,
subs: []string{"example.com", "some_separator__underscore/myimage"},
},
{
input: "example.com/__underscore/myimage",
match: false,
},
{
input: "example.com/..dots/myimage",
match: false,
},
{
input: "example.com/.dots/myimage",
match: false,
},
{
input: "example.com/nodouble..dots/myimage",
match: false,
},
{
input: "example.com/nodouble..dots/myimage",
match: false,
},
{
input: "docker./docker",
match: false,
},
{
input: ".docker/docker",
match: false,
},
{
input: "docker-/docker",
match: false,
},
{
input: "-docker/docker",
match: false,
},
{
input: "do..cker/docker",
match: false,
},
{
input: "do__cker:8080/docker",
match: false,
},
{
input: "do__cker/docker",
match: true,
subs: []string{"", "do__cker/docker"},
},
{
input: "b.gcr.io/test.example.com/my-app",
match: true,
subs: []string{"b.gcr.io", "test.example.com/my-app"},
},
{
input: "registry.io/foo/project--id.module--name.ver---sion--name",
match: true,
subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"},
},
{
input: "Asdf.com/foo/bar", // uppercase character in hostname
match: true,
},
{
input: "Foo/FarB", // uppercase characters in remote name
match: false,
},
}
for i := range testcases {
checkRegexp(t, anchoredNameRegexp, testcases[i])
}
}
func TestReferenceRegexp(t *testing.T) {
if ReferenceRegexp.NumSubexp() != 3 {
t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3",
ReferenceRegexp, ReferenceRegexp.NumSubexp())
}
testcases := []regexpMatch{
{
input: "registry.com:8080/myapp:tag",
match: true,
subs: []string{"registry.com:8080/myapp", "tag", ""},
},
{
input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@sha256:badbadbadbad",
match: false,
},
{
input: "registry.com:8080/myapp:invalid~tag",
match: false,
},
{
input: "bad_hostname.com:8080/myapp:tag",
match: false,
},
{
input:// localhost treated as name, missing tag with 8080 as tag
"localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: false,
},
{
// localhost will be treated as an image name without a host
input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912",
match: true,
subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"},
},
{
input: "registry.com:8080/myapp@bad",
match: false,
},
{
input: "registry.com:8080/myapp@2bad",
match: false, // TODO(dmcgowan): Support this as valid
},
}
for i := range testcases {
checkRegexp(t, ReferenceRegexp, testcases[i])
}
}
func TestIdentifierRegexp(t *testing.T) {
fullCases := []regexpMatch{
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: true,
},
{
input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf",
match: false,
},
{
input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821",
match: false,
},
{
input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482",
match: false,
},
}
for i := range fullCases {
checkRegexp(t, anchoredIdentifierRegexp, fullCases[i])
}
}

View File

@@ -1,73 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"sort"
)
// Sort sorts string references preferring higher information references
// The precedence is as follows:
// 1. Name + Tag + Digest
// 2. Name + Tag
// 3. Name + Digest
// 4. Name
// 5. Digest
// 6. Parse error
func Sort(references []string) []string {
var prefs []Reference
var bad []string
for _, ref := range references {
pref, err := ParseAnyReference(ref)
if err != nil {
bad = append(bad, ref)
} else {
prefs = append(prefs, pref)
}
}
sort.Slice(prefs, func(a, b int) bool {
ar := refRank(prefs[a])
br := refRank(prefs[b])
if ar == br {
return prefs[a].String() < prefs[b].String()
}
return ar < br
})
sort.Strings(bad)
var refs []string
for _, pref := range prefs {
refs = append(refs, pref.String())
}
return append(refs, bad...)
}
func refRank(ref Reference) uint8 {
if _, ok := ref.(Named); ok {
if _, ok = ref.(Tagged); ok {
if _, ok = ref.(Digested); ok {
return 1
}
return 2
}
if _, ok = ref.(Digested); ok {
return 3
}
return 4
}
return 5
}

View File

@@ -0,0 +1,26 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import "github.com/distribution/reference"
// Sort sorts string references preferring higher information references.
//
// Deprecated: use [reference.Sort].
func Sort(references []string) []string {
return reference.Sort(references)
}

View File

@@ -1,83 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package docker
import (
"io"
"math/rand"
"testing"
"github.com/opencontainers/go-digest"
)
func TestReferenceSorting(t *testing.T) {
digested := func(seed int64) string {
b, err := io.ReadAll(io.LimitReader(rand.New(rand.NewSource(seed)), 64))
if err != nil {
panic(err)
}
return digest.FromBytes(b).String()
}
// Add z. prefix to string sort after "sha256:"
r1 := func(name, tag string, seed int64) string {
return "z.containerd.io/" + name + ":" + tag + "@" + digested(seed)
}
r2 := func(name, tag string) string {
return "z.containerd.io/" + name + ":" + tag
}
r3 := func(name string, seed int64) string {
return "z.containerd.io/" + name + "@" + digested(seed)
}
for i, tc := range []struct {
unsorted []string
expected []string
}{
{
unsorted: []string{r2("name", "latest"), r3("name", 1), r1("name", "latest", 1)},
expected: []string{r1("name", "latest", 1), r2("name", "latest"), r3("name", 1)},
},
{
unsorted: []string{"can't parse this:latest", r3("name", 1), r2("name", "latest")},
expected: []string{r2("name", "latest"), r3("name", 1), "can't parse this:latest"},
},
{
unsorted: []string{digested(1), r3("name", 1), r2("name", "latest")},
expected: []string{r2("name", "latest"), r3("name", 1), digested(1)},
},
{
unsorted: []string{r2("name", "tag2"), r2("name", "tag3"), r2("name", "tag1")},
expected: []string{r2("name", "tag1"), r2("name", "tag2"), r2("name", "tag3")},
},
{
unsorted: []string{r2("name-2", "tag"), r2("name-3", "tag"), r2("name-1", "tag")},
expected: []string{r2("name-1", "tag"), r2("name-2", "tag"), r2("name-3", "tag")},
},
} {
sorted := Sort(tc.unsorted)
if len(sorted) != len(tc.expected) {
t.Errorf("[%d]: Mismatched sized, got %d, expected %d", i, len(sorted), len(tc.expected))
continue
}
for j := range sorted {
if sorted[j] != tc.expected[j] {
t.Errorf("[%d]: Wrong value at %d, got %q, expected %q", i, j, sorted[j], tc.expected[j])
break
}
}
}
}