diff --git a/directory/directory_transport.go b/directory/directory_transport.go index 5703127f19..ca09c03ee1 100644 --- a/directory/directory_transport.go +++ b/directory/directory_transport.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/containers/image/directory/explicitfilepath" + "github.com/containers/image/docker/reference" "github.com/containers/image/image" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // Transport is an ImageTransport for directory paths. diff --git a/docker/docker_transport.go b/docker/docker_transport.go index 52c4ab73cf..1284dc4bfe 100644 --- a/docker/docker_transport.go +++ b/docker/docker_transport.go @@ -5,8 +5,8 @@ import ( "strings" "github.com/containers/image/docker/policyconfiguration" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // Transport is an ImageTransport for Docker registry-hosted images. diff --git a/docker/docker_transport_test.go b/docker/docker_transport_test.go index eb71f620cd..c6c83623b8 100644 --- a/docker/docker_transport_test.go +++ b/docker/docker_transport_test.go @@ -3,8 +3,8 @@ package docker import ( "testing" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/docker/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/docker/policyconfiguration/naming.go b/docker/policyconfiguration/naming.go index ace300e43d..08892cb16f 100644 --- a/docker/policyconfiguration/naming.go +++ b/docker/policyconfiguration/naming.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/docker/docker/reference" + "github.com/containers/image/docker/reference" ) // DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup, diff --git a/docker/policyconfiguration/naming_test.go b/docker/policyconfiguration/naming_test.go index bca116782e..0269db95cf 100644 --- a/docker/policyconfiguration/naming_test.go +++ b/docker/policyconfiguration/naming_test.go @@ -6,7 +6,7 @@ import ( "fmt" - "github.com/docker/docker/reference" + "github.com/containers/image/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/docker/reference/doc.go b/docker/reference/doc.go new file mode 100644 index 0000000000..a75ea749e5 --- /dev/null +++ b/docker/reference/doc.go @@ -0,0 +1,6 @@ +// Package reference is a fork of the upstream docker/docker/reference package. +// The package is forked because we need consistency especially when storing and +// checking signatures (RH patches break this consistency because they modify +// docker/docker/reference as part of a patch carried in projectatomic/docker). +// The version of this package is v1.12.1 from upstream, update as necessary. +package reference diff --git a/docker/reference/reference.go b/docker/reference/reference.go new file mode 100644 index 0000000000..de26e44c0f --- /dev/null +++ b/docker/reference/reference.go @@ -0,0 +1,220 @@ +package reference + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/docker/distribution/digest" + distreference "github.com/docker/distribution/reference" +) + +const ( + // DefaultTag defines the default tag used when performing images related actions and no tag or digest is specified + DefaultTag = "latest" + // DefaultHostname is the default built-in hostname + DefaultHostname = "docker.io" + // LegacyDefaultHostname is automatically converted to DefaultHostname + LegacyDefaultHostname = "index.docker.io" + // DefaultRepoPrefix is the prefix used for default repositories in default host + DefaultRepoPrefix = "library/" +) + +// Named is an object with a full name +type Named interface { + // Name returns normalized repository name, like "ubuntu". + Name() string + // String returns full reference, like "ubuntu@sha256:abcdef..." + String() string + // FullName returns full repository name with hostname, like "docker.io/library/ubuntu" + FullName() string + // Hostname returns hostname for the reference, like "docker.io" + Hostname() string + // RemoteName returns the repository component of the full name, like "library/ubuntu" + RemoteName() string +} + +// NamedTagged is an object including a name and tag. +type NamedTagged interface { + Named + Tag() string +} + +// Canonical reference is an object with a fully unique +// name including a name with hostname and digest +type Canonical interface { + Named + Digest() digest.Digest +} + +// ParseNamed parses s and returns a syntactically valid reference implementing +// the Named interface. The reference must have a name, otherwise an error is +// returned. +// If an error was encountered it is returned, along with a nil Reference. +func ParseNamed(s string) (Named, error) { + named, err := distreference.ParseNamed(s) + if err != nil { + return nil, fmt.Errorf("Error parsing reference: %q is not a valid repository/tag", s) + } + r, err := WithName(named.Name()) + if err != nil { + return nil, err + } + if canonical, isCanonical := named.(distreference.Canonical); isCanonical { + return WithDigest(r, canonical.Digest()) + } + if tagged, isTagged := named.(distreference.NamedTagged); isTagged { + return WithTag(r, tagged.Tag()) + } + return r, 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) { + name, err := normalize(name) + if err != nil { + return nil, err + } + if err := validateName(name); err != nil { + return nil, err + } + r, err := distreference.WithName(name) + if err != nil { + return nil, err + } + return &namedRef{r}, 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) { + r, err := distreference.WithTag(name, tag) + if err != nil { + return nil, err + } + return &taggedRef{namedRef{r}}, 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) { + r, err := distreference.WithDigest(name, digest) + if err != nil { + return nil, err + } + return &canonicalRef{namedRef{r}}, nil +} + +type namedRef struct { + distreference.Named +} +type taggedRef struct { + namedRef +} +type canonicalRef struct { + namedRef +} + +func (r *namedRef) FullName() string { + hostname, remoteName := splitHostname(r.Name()) + return hostname + "/" + remoteName +} +func (r *namedRef) Hostname() string { + hostname, _ := splitHostname(r.Name()) + return hostname +} +func (r *namedRef) RemoteName() string { + _, remoteName := splitHostname(r.Name()) + return remoteName +} +func (r *taggedRef) Tag() string { + return r.namedRef.Named.(distreference.NamedTagged).Tag() +} +func (r *canonicalRef) Digest() digest.Digest { + return r.namedRef.Named.(distreference.Canonical).Digest() +} + +// WithDefaultTag adds a default tag to a reference if it only has a repo name. +func WithDefaultTag(ref Named) Named { + if IsNameOnly(ref) { + ref, _ = WithTag(ref, DefaultTag) + } + return ref +} + +// 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 +} + +// ParseIDOrReference parses string for an image ID or a reference. ID can be +// without a default prefix. +func ParseIDOrReference(idOrRef string) (digest.Digest, Named, error) { + if err := validateID(idOrRef); err == nil { + idOrRef = "sha256:" + idOrRef + } + if dgst, err := digest.ParseDigest(idOrRef); err == nil { + return dgst, nil, nil + } + ref, err := ParseNamed(idOrRef) + return "", ref, err +} + +// splitHostname splits a repository name to hostname and remotename string. +// If no valid hostname is found, the default hostname is used. Repository name +// needs to be already validated before. +func splitHostname(name string) (hostname, remoteName string) { + i := strings.IndexRune(name, '/') + if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") { + hostname, remoteName = DefaultHostname, name + } else { + hostname, remoteName = name[:i], name[i+1:] + } + if hostname == LegacyDefaultHostname { + hostname = DefaultHostname + } + if hostname == DefaultHostname && !strings.ContainsRune(remoteName, '/') { + remoteName = DefaultRepoPrefix + remoteName + } + return +} + +// normalize returns a repository name in its normalized form, meaning it +// will not contain default hostname nor library/ prefix for official images. +func normalize(name string) (string, error) { + host, remoteName := splitHostname(name) + if strings.ToLower(remoteName) != remoteName { + return "", errors.New("invalid reference format: repository name must be lowercase") + } + if host == DefaultHostname { + if strings.HasPrefix(remoteName, DefaultRepoPrefix) { + return strings.TrimPrefix(remoteName, DefaultRepoPrefix), nil + } + return remoteName, nil + } + return name, nil +} + +var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`) + +func validateID(id string) error { + if ok := validHex.MatchString(id); !ok { + return fmt.Errorf("image ID %q is invalid", id) + } + return nil +} + +func validateName(name string) error { + if err := validateID(name); err == nil { + return fmt.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) + } + return nil +} diff --git a/docker/reference/reference_test.go b/docker/reference/reference_test.go new file mode 100644 index 0000000000..ff35ba3da2 --- /dev/null +++ b/docker/reference/reference_test.go @@ -0,0 +1,275 @@ +package reference + +import ( + "testing" + + "github.com/docker/distribution/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", + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + } + 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", + "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + "docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + } + + for _, name := range invalidRepoNames { + _, err := ParseNamed(name) + if err == nil { + t.Fatalf("Expected invalid repo name for %q", name) + } + } + + for _, name := range validRepoNames { + _, err := ParseNamed(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 := ParseNamed(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 := ParseNamed(repositoryName); err == nil { + t.Errorf("Repository name should be invalid: %v", repositoryName) + } + } +} + +func TestParseRepositoryInfo(t *testing.T) { + type tcase struct { + RemoteName, NormalizedName, FullName, AmbiguousName, Hostname string + } + + tcases := []tcase{ + { + RemoteName: "fooo/bar", + NormalizedName: "fooo/bar", + FullName: "docker.io/fooo/bar", + AmbiguousName: "index.docker.io/fooo/bar", + Hostname: "docker.io", + }, + { + RemoteName: "library/ubuntu", + NormalizedName: "ubuntu", + FullName: "docker.io/library/ubuntu", + AmbiguousName: "library/ubuntu", + Hostname: "docker.io", + }, + { + RemoteName: "nonlibrary/ubuntu", + NormalizedName: "nonlibrary/ubuntu", + FullName: "docker.io/nonlibrary/ubuntu", + AmbiguousName: "", + Hostname: "docker.io", + }, + { + RemoteName: "other/library", + NormalizedName: "other/library", + FullName: "docker.io/other/library", + AmbiguousName: "", + Hostname: "docker.io", + }, + { + RemoteName: "private/moonbase", + NormalizedName: "127.0.0.1:8000/private/moonbase", + FullName: "127.0.0.1:8000/private/moonbase", + AmbiguousName: "", + Hostname: "127.0.0.1:8000", + }, + { + RemoteName: "privatebase", + NormalizedName: "127.0.0.1:8000/privatebase", + FullName: "127.0.0.1:8000/privatebase", + AmbiguousName: "", + Hostname: "127.0.0.1:8000", + }, + { + RemoteName: "private/moonbase", + NormalizedName: "example.com/private/moonbase", + FullName: "example.com/private/moonbase", + AmbiguousName: "", + Hostname: "example.com", + }, + { + RemoteName: "privatebase", + NormalizedName: "example.com/privatebase", + FullName: "example.com/privatebase", + AmbiguousName: "", + Hostname: "example.com", + }, + { + RemoteName: "private/moonbase", + NormalizedName: "example.com:8000/private/moonbase", + FullName: "example.com:8000/private/moonbase", + AmbiguousName: "", + Hostname: "example.com:8000", + }, + { + RemoteName: "privatebasee", + NormalizedName: "example.com:8000/privatebasee", + FullName: "example.com:8000/privatebasee", + AmbiguousName: "", + Hostname: "example.com:8000", + }, + { + RemoteName: "library/ubuntu-12.04-base", + NormalizedName: "ubuntu-12.04-base", + FullName: "docker.io/library/ubuntu-12.04-base", + AmbiguousName: "index.docker.io/library/ubuntu-12.04-base", + Hostname: "docker.io", + }, + } + + for _, tcase := range tcases { + refStrings := []string{tcase.NormalizedName, tcase.FullName} + if tcase.AmbiguousName != "" { + refStrings = append(refStrings, tcase.AmbiguousName) + } + + var refs []Named + for _, r := range refStrings { + named, err := ParseNamed(r) + if err != nil { + t.Fatal(err) + } + refs = append(refs, named) + named, err = WithName(r) + if err != nil { + t.Fatal(err) + } + refs = append(refs, named) + } + + for _, r := range refs { + if expected, actual := tcase.NormalizedName, r.Name(); expected != actual { + t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.FullName, r.FullName(); expected != actual { + t.Fatalf("Invalid normalized reference for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.Hostname, r.Hostname(); expected != actual { + t.Fatalf("Invalid hostname for %q. Expected %q, got %q", r, expected, actual) + } + if expected, actual := tcase.RemoteName, r.RemoteName(); expected != actual { + t.Fatalf("Invalid remoteName for %q. Expected %q, got %q", r, expected, actual) + } + + } + } +} + +func TestParseReferenceWithTagAndDigest(t *testing.T) { + ref, err := ParseNamed("busybox:latest@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa") + if err != nil { + t.Fatal(err) + } + if _, isTagged := ref.(NamedTagged); isTagged { + t.Fatalf("Reference from %q should not support tag", ref) + } + if _, isCanonical := ref.(Canonical); !isCanonical { + t.Fatalf("Reference from %q should not support digest", ref) + } + if expected, actual := "busybox@sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa", ref.String(); actual != expected { + t.Fatalf("Invalid parsed reference for %q: expected %q, got %q", ref, expected, actual) + } +} + +func TestInvalidReferenceComponents(t *testing.T) { + if _, err := WithName("-foo"); err == nil { + t.Fatal("Expected WithName to detect invalid name") + } + ref, err := WithName("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 WithName to detect invalid digest") + } +} diff --git a/oci/layout/oci_transport.go b/oci/layout/oci_transport.go index fcd4c53145..4124684b52 100644 --- a/oci/layout/oci_transport.go +++ b/oci/layout/oci_transport.go @@ -8,8 +8,8 @@ import ( "strings" "github.com/containers/image/directory/explicitfilepath" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // Transport is an ImageTransport for OCI directories. diff --git a/openshift/openshift_transport.go b/openshift/openshift_transport.go index 213f1f72be..9e2b106ce4 100644 --- a/openshift/openshift_transport.go +++ b/openshift/openshift_transport.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/containers/image/docker/policyconfiguration" + "github.com/containers/image/docker/reference" genericImage "github.com/containers/image/image" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // Transport is an ImageTransport for OpenShift registry-hosted images. diff --git a/openshift/openshift_transport_test.go b/openshift/openshift_transport_test.go index f54fc6dc6b..46904ff369 100644 --- a/openshift/openshift_transport_test.go +++ b/openshift/openshift_transport_test.go @@ -3,7 +3,7 @@ package openshift import ( "testing" - "github.com/docker/docker/reference" + "github.com/containers/image/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/signature/policy_config.go b/signature/policy_config.go index f50a75cc88..d5e20489bd 100644 --- a/signature/policy_config.go +++ b/signature/policy_config.go @@ -20,9 +20,9 @@ import ( "io/ioutil" "path/filepath" + "github.com/containers/image/docker/reference" "github.com/containers/image/transports" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // systemDefaultPolicyPath is the policy path used for DefaultPolicy(). diff --git a/signature/policy_eval_signedby_test.go b/signature/policy_eval_signedby_test.go index cc9af6f987..d21ee9c17f 100644 --- a/signature/policy_eval_signedby_test.go +++ b/signature/policy_eval_signedby_test.go @@ -7,9 +7,9 @@ import ( "testing" "github.com/containers/image/directory" + "github.com/containers/image/docker/reference" "github.com/containers/image/image" "github.com/containers/image/types" - "github.com/docker/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/signature/policy_eval_simple_test.go b/signature/policy_eval_simple_test.go index b3ef759f49..aae4b6a8b8 100644 --- a/signature/policy_eval_simple_test.go +++ b/signature/policy_eval_simple_test.go @@ -3,8 +3,8 @@ package signature import ( "testing" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // nameOnlyImageMock is a mock of types.UnparsedImage which only allows transports.ImageName to work diff --git a/signature/policy_eval_test.go b/signature/policy_eval_test.go index 15bb13df6e..e24eee8ad5 100644 --- a/signature/policy_eval_test.go +++ b/signature/policy_eval_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/containers/image/docker/policyconfiguration" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/docker/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/signature/policy_reference_match.go b/signature/policy_reference_match.go index df8eacea4f..aedda8d09b 100644 --- a/signature/policy_reference_match.go +++ b/signature/policy_reference_match.go @@ -5,9 +5,9 @@ package signature import ( "fmt" + "github.com/containers/image/docker/reference" "github.com/containers/image/transports" "github.com/containers/image/types" - "github.com/docker/docker/reference" ) // parseImageAndDockerReference converts an image and a reference string into two parsed entities, failing on any error and handling unidentified images. diff --git a/signature/policy_reference_match_test.go b/signature/policy_reference_match_test.go index a0967e713e..784cd3b669 100644 --- a/signature/policy_reference_match_test.go +++ b/signature/policy_reference_match_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/docker/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/types/types.go b/types/types.go index e1c7ec019c..2e8546d73e 100644 --- a/types/types.go +++ b/types/types.go @@ -4,7 +4,7 @@ import ( "io" "time" - "github.com/docker/docker/reference" + "github.com/containers/image/docker/reference" ) // ImageTransport is a top-level namespace for ways to to store/load an image.