diff --git a/docker/daemon/daemon_dest.go b/docker/daemon/daemon_dest.go index 816a62261e..dd2d725bca 100644 --- a/docker/daemon/daemon_dest.go +++ b/docker/daemon/daemon_dest.go @@ -230,7 +230,7 @@ func (d *daemonImageDestination) PutManifest(m []byte) error { // a hostname-qualified reference. // See https://github.com/containers/image/issues/72 for a more detailed // analysis and explanation. - refString := fmt.Sprintf("%s:%s", d.namedTaggedRef.FullName(), d.namedTaggedRef.Tag()) + refString := fmt.Sprintf("%s:%s", d.namedTaggedRef.Name(), d.namedTaggedRef.Tag()) items := []manifestItem{{ Config: man.Config.Digest.String(), diff --git a/docker/daemon/daemon_transport.go b/docker/daemon/daemon_transport.go index c8e40aed28..d64f088f00 100644 --- a/docker/daemon/daemon_transport.go +++ b/docker/daemon/daemon_transport.go @@ -46,11 +46,11 @@ type daemonReference struct { // ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an ImageReference. func ParseReference(refString string) (types.ImageReference, error) { - // This is intended to be compatible with reference.ParseIDOrReference, but more strict about refusing some of the ambiguous cases. + // This is intended to be compatible with reference.ParseAnyReference, but more strict about refusing some of the ambiguous cases. // In particular, this rejects unprefixed digest values (64 hex chars), and sha256 digest prefixes (sha256:fewer-than-64-hex-chars). // digest:hexstring is structurally the same as a reponame:tag (meaning docker.io/library/reponame:tag). - // reference.ParseIDOrReference interprets such strings as digests. + // reference.ParseAnyReference interprets such strings as digests. if dgst, err := digest.Parse(refString); err == nil { // The daemon explicitly refuses to tag images with a reponame equal to digest.Canonical - but _only_ this digest name. // Other digest references are ambiguous, so refuse them. @@ -60,11 +60,11 @@ func ParseReference(refString string) (types.ImageReference, error) { return NewReference(dgst, nil) } - ref, err := reference.ParseNamed(refString) // This also rejects unprefixed digest values + ref, err := reference.ParseNormalizedNamed(refString) // This also rejects unprefixed digest values if err != nil { return nil, err } - if ref.Name() == digest.Canonical.String() { + if reference.FamiliarName(ref) == digest.Canonical.String() { return nil, errors.Errorf("Invalid docker-daemon: reference %s: The %s repository name is reserved for (non-shortened) digest references", refString, digest.Canonical) } return NewReference("", ref) @@ -77,10 +77,11 @@ func NewReference(id digest.Digest, ref reference.Named) (types.ImageReference, } if ref != nil { if reference.IsNameOnly(ref) { - return nil, errors.Errorf("docker-daemon: reference %s has neither a tag nor a digest", ref.String()) + return nil, errors.Errorf("docker-daemon: reference %s has neither a tag nor a digest", reference.FamiliarString(ref)) } // A github.com/distribution/reference value can have a tag and a digest at the same time! - // docker/reference does not handle that, so fail. + // Most versions of docker/reference do not handle that (ignoring the tag), so reject such input. + // This MAY be accepted in the future. _, isTagged := ref.(reference.NamedTagged) _, isDigested := ref.(reference.Canonical) if isTagged && isDigested { @@ -108,7 +109,7 @@ func (ref daemonReference) StringWithinTransport() string { case ref.id != "": return ref.id.String() case ref.ref != nil: - return ref.ref.String() + return reference.FamiliarString(ref.ref) default: // Coverage: Should never happen, NewReference above should refuse such values. panic("Internal inconsistency: daemonReference has empty id and nil ref") } diff --git a/docker/daemon/daemon_transport_test.go b/docker/daemon/daemon_transport_test.go index 68ad255367..2a60c6b29c 100644 --- a/docker/daemon/daemon_transport_test.go +++ b/docker/daemon/daemon_transport_test.go @@ -50,15 +50,12 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err {"sha256:XX23456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "", ""}, // Invalid digest value {"UPPERCASEISINVALID", "", ""}, // Invalid reference input {"busybox", "", ""}, // Missing tag or digest - {"busybox:latest", "", "busybox:latest"}, // Explicit tag - {"busybox@" + sha256digest, "", "busybox@" + sha256digest}, // Explicit digest + {"busybox:latest", "", "docker.io/library/busybox:latest"}, // Explicit tag + {"busybox@" + sha256digest, "", "docker.io/library/busybox@" + sha256digest}, // Explicit digest // A github.com/distribution/reference value can have a tag and a digest at the same time! - // github.com/docker/reference handles that by dropping the tag. That is not obviously the - // right thing to do, but it is at least reasonable, so test that we keep behaving reasonably. - // This test case should not be construed to make this an API promise. - // FIXME? Instead work extra hard to reject such input? - {"busybox:latest@" + sha256digest, "", "busybox@" + sha256digest}, // Both tag and digest - {"docker.io/library/busybox:latest", "", "busybox:latest"}, // All implied values explicitly specified + // Most versions of docker/reference do not handle that (ignoring the tag), so we reject such input. + {"busybox:latest@" + sha256digest, "", ""}, // Both tag and digest + {"docker.io/library/busybox:latest", "", "docker.io/library/busybox:latest"}, // All implied values explicitly specified } { ref, err := fn(c.input) if c.expectedID == "" && c.expectedRef == "" { @@ -67,43 +64,37 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err require.NoError(t, err, c.input) daemonRef, ok := ref.(daemonReference) require.True(t, ok, c.input) - // If we don't reject the input, the interpretation must be consistent for reference.ParseIDOrReference - dockerID, dockerRef, err := reference.ParseIDOrReference(c.input) + // If we don't reject the input, the interpretation must be consistent with reference.ParseAnyReference + dockerRef, err := reference.ParseAnyReference(c.input) require.NoError(t, err, c.input) if c.expectedRef == "" { assert.Equal(t, c.expectedID, daemonRef.id.String(), c.input) assert.Nil(t, daemonRef.ref, c.input) - assert.Equal(t, c.expectedID, dockerID.String(), c.input) - assert.Nil(t, dockerRef, c.input) + _, ok := dockerRef.(reference.Digested) + require.True(t, ok, c.input) + assert.Equal(t, c.expectedID, dockerRef.String(), c.input) } else { assert.Equal(t, "", daemonRef.id.String(), c.input) require.NotNil(t, daemonRef.ref, c.input) assert.Equal(t, c.expectedRef, daemonRef.ref.String(), c.input) - assert.Equal(t, "", dockerID.String(), c.input) - require.NotNil(t, dockerRef, c.input) + _, ok := dockerRef.(reference.Named) + require.True(t, ok, c.input) assert.Equal(t, c.expectedRef, dockerRef.String(), c.input) } } } } -// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time. -type refWithTagAndDigest struct{ reference.Canonical } - -func (ref refWithTagAndDigest) Tag() string { - return "notLatest" -} - // A common list of reference formats to test for the various ImageReference methods. // (For IDs it is much simpler, we simply use them unmodified) var validNamedReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ - {"busybox:notlatest", "busybox:notlatest", "busybox:notlatest"}, // Explicit tag - {"busybox" + sha256digest, "busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest - {"docker.io/library/busybox:latest", "busybox:latest", "busybox:latest"}, // All implied values explicitly specified - {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "example.com/ns/foo:bar"}, // All values explicitly specified + {"busybox:notlatest", "docker.io/library/busybox:notlatest", "busybox:notlatest"}, // Explicit tag + {"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest + {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified + {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "example.com/ns/foo:bar"}, // All values explicitly specified } func TestNewReference(t *testing.T) { @@ -119,7 +110,7 @@ func TestNewReference(t *testing.T) { // Named references for _, c := range validNamedReferenceTestCases { - parsed, err := reference.ParseNamed(c.input) + parsed, err := reference.ParseNormalizedNamed(c.input) require.NoError(t, err) ref, err := NewReference("", parsed) require.NoError(t, err, c.input) @@ -131,24 +122,25 @@ func TestNewReference(t *testing.T) { } // Both an ID and a named reference provided - parsed, err := reference.ParseNamed("busybox:latest") + parsed, err := reference.ParseNormalizedNamed("busybox:latest") require.NoError(t, err) _, err = NewReference(id, parsed) assert.Error(t, err) // A reference with neither a tag nor digest - parsed, err = reference.ParseNamed("busybox") + parsed, err = reference.ParseNormalizedNamed("busybox") require.NoError(t, err) _, err = NewReference("", parsed) assert.Error(t, err) // A github.com/distribution/reference value can have a tag and a digest at the same time! - parsed, err = reference.ParseNamed("busybox@" + sha256digest) + parsed, err = reference.ParseNormalizedNamed("busybox:notlatest@" + sha256digest) require.NoError(t, err) - refDigested, ok := parsed.(reference.Canonical) + _, ok = parsed.(reference.Canonical) require.True(t, ok) - tagDigestRef := refWithTagAndDigest{refDigested} - _, err = NewReference("", tagDigestRef) + _, ok = parsed.(reference.NamedTagged) + require.True(t, ok) + _, err = NewReference("", parsed) assert.Error(t, err) } diff --git a/docker/docker_client.go b/docker/docker_client.go index 0605cad2e1..4e9fe5756f 100644 --- a/docker/docker_client.go +++ b/docker/docker_client.go @@ -15,6 +15,7 @@ import ( "time" "github.com/Sirupsen/logrus" + "github.com/containers/image/docker/reference" "github.com/containers/image/types" "github.com/containers/storage/pkg/homedir" "github.com/docker/go-connections/sockets" @@ -164,11 +165,11 @@ func hasFile(files []os.FileInfo, name string) bool { // newDockerClient returns a new dockerClient instance for refHostname (a host a specified in the Docker image reference, not canonicalized to dockerRegistry) // “write” specifies whether the client will be used for "write" access (in particular passed to lookaside.go:toplevelFromSection) func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, actions string) (*dockerClient, error) { - registry := ref.ref.Hostname() + registry := reference.Domain(ref.ref) if registry == dockerHostname { registry = dockerRegistry } - username, password, err := getAuth(ctx, ref.ref.Hostname()) + username, password, err := getAuth(ctx, reference.Domain(ref.ref)) if err != nil { return nil, err } @@ -202,7 +203,7 @@ func newDockerClient(ctx *types.SystemContext, ref dockerReference, write bool, signatureBase: sigBase, scope: authScope{ actions: actions, - remoteName: ref.ref.RemoteName(), + remoteName: reference.Path(ref.ref), }, }, nil } diff --git a/docker/docker_image.go b/docker/docker_image.go index ce769c0a3c..2bea7eb643 100644 --- a/docker/docker_image.go +++ b/docker/docker_image.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" + "github.com/containers/image/docker/reference" "github.com/containers/image/image" "github.com/containers/image/types" "github.com/pkg/errors" @@ -34,12 +35,12 @@ func newImage(ctx *types.SystemContext, ref dockerReference) (types.Image, error // SourceRefFullName returns a fully expanded name for the repository this image is in. func (i *Image) SourceRefFullName() string { - return i.src.ref.ref.FullName() + return i.src.ref.ref.Name() } // GetRepositoryTags list all tags available in the repository. Note that this has no connection with the tag(s) used for this specific image, if any. func (i *Image) GetRepositoryTags() ([]string, error) { - url := fmt.Sprintf(tagsURL, i.src.ref.ref.RemoteName()) + url := fmt.Sprintf(tagsURL, reference.Path(i.src.ref.ref)) res, err := i.src.c.makeRequest("GET", url, nil, nil) if err != nil { return nil, err diff --git a/docker/docker_image_dest.go b/docker/docker_image_dest.go index 78ccc27afb..4c5ee1436c 100644 --- a/docker/docker_image_dest.go +++ b/docker/docker_image_dest.go @@ -11,6 +11,7 @@ import ( "path/filepath" "github.com/Sirupsen/logrus" + "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/opencontainers/go-digest" @@ -98,7 +99,7 @@ func (c *sizeCounter) Write(p []byte) (n int, err error) { // If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far. func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobInfo) (types.BlobInfo, error) { if inputInfo.Digest.String() != "" { - checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), inputInfo.Digest.String()) + checkURL := fmt.Sprintf(blobsURL, reference.Path(d.ref.ref), inputInfo.Digest.String()) logrus.Debugf("Checking %s", checkURL) res, err := d.c.makeRequest("HEAD", checkURL, nil, nil) @@ -112,17 +113,17 @@ func (d *dockerImageDestination) PutBlob(stream io.Reader, inputInfo types.BlobI return types.BlobInfo{Digest: inputInfo.Digest, Size: getBlobSize(res)}, nil case http.StatusUnauthorized: logrus.Debugf("... not authorized") - return types.BlobInfo{}, errors.Errorf("not authorized to read from destination repository %s", d.ref.ref.RemoteName()) + return types.BlobInfo{}, errors.Errorf("not authorized to read from destination repository %s", reference.Path(d.ref.ref)) case http.StatusNotFound: // noop default: - return types.BlobInfo{}, errors.Errorf("failed to read from destination repository %s: %v", d.ref.ref.RemoteName(), http.StatusText(res.StatusCode)) + return types.BlobInfo{}, errors.Errorf("failed to read from destination repository %s: %v", reference.Path(d.ref.ref), http.StatusText(res.StatusCode)) } logrus.Debugf("... failed, status %d", res.StatusCode) } // FIXME? Chunked upload, progress reporting, etc. - uploadURL := fmt.Sprintf(blobUploadURL, d.ref.ref.RemoteName()) + uploadURL := fmt.Sprintf(blobUploadURL, reference.Path(d.ref.ref)) logrus.Debugf("Uploading %s", uploadURL) res, err := d.c.makeRequest("POST", uploadURL, nil, nil) if err != nil { @@ -178,7 +179,7 @@ func (d *dockerImageDestination) HasBlob(info types.BlobInfo) (bool, int64, erro if info.Digest == "" { return false, -1, errors.Errorf(`"Can not check for a blob with unknown digest`) } - checkURL := fmt.Sprintf(blobsURL, d.ref.ref.RemoteName(), info.Digest.String()) + checkURL := fmt.Sprintf(blobsURL, reference.Path(d.ref.ref), info.Digest.String()) logrus.Debugf("Checking %s", checkURL) res, err := d.c.makeRequest("HEAD", checkURL, nil, nil) @@ -192,12 +193,12 @@ func (d *dockerImageDestination) HasBlob(info types.BlobInfo) (bool, int64, erro return true, getBlobSize(res), nil case http.StatusUnauthorized: logrus.Debugf("... not authorized") - return false, -1, errors.Errorf("not authorized to read from destination repository %s", d.ref.ref.RemoteName()) + return false, -1, errors.Errorf("not authorized to read from destination repository %s", reference.Path(d.ref.ref)) case http.StatusNotFound: logrus.Debugf("... not present") return false, -1, types.ErrBlobNotFound default: - logrus.Errorf("failed to read from destination repository %s: %v", d.ref.ref.RemoteName(), http.StatusText(res.StatusCode)) + logrus.Errorf("failed to read from destination repository %s: %v", reference.Path(d.ref.ref), http.StatusText(res.StatusCode)) } logrus.Debugf("... failed, status %d, ignoring", res.StatusCode) return false, -1, types.ErrBlobNotFound @@ -214,11 +215,11 @@ func (d *dockerImageDestination) PutManifest(m []byte) error { } d.manifestDigest = digest - reference, err := d.ref.tagOrDigest() + refTail, err := d.ref.tagOrDigest() if err != nil { return err } - url := fmt.Sprintf(manifestURL, d.ref.ref.RemoteName(), reference) + url := fmt.Sprintf(manifestURL, reference.Path(d.ref.ref), refTail) headers := map[string][]string{} mimeType := manifest.GuessMIMEType(m) diff --git a/docker/docker_image_src.go b/docker/docker_image_src.go index f87a5fc23c..e26d0f0b53 100644 --- a/docker/docker_image_src.go +++ b/docker/docker_image_src.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/Sirupsen/logrus" + "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/docker/distribution/registry/client" @@ -91,7 +92,7 @@ func (s *dockerImageSource) GetManifest() ([]byte, string, error) { } func (s *dockerImageSource) fetchManifest(tagOrDigest string) ([]byte, string, error) { - url := fmt.Sprintf(manifestURL, s.ref.ref.RemoteName(), tagOrDigest) + url := fmt.Sprintf(manifestURL, reference.Path(s.ref.ref), tagOrDigest) headers := make(map[string][]string) headers["Accept"] = s.requestedManifestMIMETypes res, err := s.c.makeRequest("GET", url, headers, nil) @@ -177,7 +178,7 @@ func (s *dockerImageSource) GetBlob(info types.BlobInfo) (io.ReadCloser, int64, return s.getExternalBlob(info.URLs) } - url := fmt.Sprintf(blobsURL, s.ref.ref.RemoteName(), info.Digest.String()) + url := fmt.Sprintf(blobsURL, reference.Path(s.ref.ref), info.Digest.String()) logrus.Debugf("Downloading %s", url) res, err := s.c.makeRequest("GET", url, nil, nil) if err != nil { @@ -271,11 +272,11 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error { headers := make(map[string][]string) headers["Accept"] = []string{manifest.DockerV2Schema2MediaType} - reference, err := ref.tagOrDigest() + refTail, err := ref.tagOrDigest() if err != nil { return err } - getURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), reference) + getURL := fmt.Sprintf(manifestURL, reference.Path(ref.ref), refTail) get, err := c.makeRequest("GET", getURL, headers, nil) if err != nil { return err @@ -294,7 +295,7 @@ func deleteImage(ctx *types.SystemContext, ref dockerReference) error { } digest := get.Header.Get("Docker-Content-Digest") - deleteURL := fmt.Sprintf(manifestURL, ref.ref.RemoteName(), digest) + deleteURL := fmt.Sprintf(manifestURL, reference.Path(ref.ref), digest) // When retrieving the digest from a registry >= 2.3 use the following header: // "Accept": "application/vnd.docker.distribution.manifest.v2+json" diff --git a/docker/docker_transport.go b/docker/docker_transport.go index 00d0b7c9bf..a8f511a0a5 100644 --- a/docker/docker_transport.go +++ b/docker/docker_transport.go @@ -45,21 +45,22 @@ func ParseReference(refString string) (types.ImageReference, error) { if !strings.HasPrefix(refString, "//") { return nil, errors.Errorf("docker: image reference %s does not start with //", refString) } - ref, err := reference.ParseNamed(strings.TrimPrefix(refString, "//")) + ref, err := reference.ParseNormalizedNamed(strings.TrimPrefix(refString, "//")) if err != nil { return nil, err } - ref = reference.WithDefaultTag(ref) + ref = reference.TagNameOnly(ref) return NewReference(ref) } // NewReference returns a Docker reference for a named reference. The reference must satisfy !reference.IsNameOnly(). func NewReference(ref reference.Named) (types.ImageReference, error) { if reference.IsNameOnly(ref) { - return nil, errors.Errorf("Docker reference %s has neither a tag nor a digest", ref.String()) + return nil, errors.Errorf("Docker reference %s has neither a tag nor a digest", reference.FamiliarString(ref)) } // A github.com/distribution/reference value can have a tag and a digest at the same time! - // docker/reference does not handle that, so fail. + // The docker/distribution API does not really support that (we can’t ask for an image with a specific + // tag and digest), so fail. This MAY be accepted in the future. // (Even if it were supported, the semantics of policy namespaces are unclear - should we drop // the tag or the digest first?) _, isTagged := ref.(reference.NamedTagged) @@ -82,7 +83,7 @@ func (ref dockerReference) Transport() types.ImageTransport { // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref dockerReference) StringWithinTransport() string { - return "//" + ref.ref.String() + return "//" + reference.FamiliarString(ref.ref) } // DockerReference returns a Docker reference associated with this reference @@ -152,5 +153,5 @@ func (ref dockerReference) tagOrDigest() (string, error) { return ref.Tag(), nil } // This should not happen, NewReference above refuses reference.IsNameOnly values. - return "", errors.Errorf("Internal inconsistency: Reference %s unexpectedly has neither a digest nor a tag", ref.ref.String()) + return "", errors.Errorf("Internal inconsistency: Reference %s unexpectedly has neither a digest nor a tag", reference.FamiliarString(ref.ref)) } diff --git a/docker/docker_transport_test.go b/docker/docker_transport_test.go index c6c83623b8..98dc7f5861 100644 --- a/docker/docker_transport_test.go +++ b/docker/docker_transport_test.go @@ -42,18 +42,16 @@ func TestParseReference(t *testing.T) { // testParseReference is a test shared for Transport.ParseReference and ParseReference. func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { for _, c := range []struct{ input, expected string }{ - {"busybox", ""}, // Missing // prefix - {"//busybox:notlatest", "busybox:notlatest"}, // Explicit tag - {"//busybox" + sha256digest, "busybox" + sha256digest}, // Explicit digest - {"//busybox", "busybox:latest"}, // Default tag + {"busybox", ""}, // Missing // prefix + {"//busybox:notlatest", "docker.io/library/busybox:notlatest"}, // Explicit tag + {"//busybox" + sha256digest, "docker.io/library/busybox" + sha256digest}, // Explicit digest + {"//busybox", "docker.io/library/busybox:latest"}, // Default tag // A github.com/distribution/reference value can have a tag and a digest at the same time! - // github.com/docker/reference handles that by dropping the tag. That is not obviously the - // right thing to do, but it is at least reasonable, so test that we keep behaving reasonably. - // This test case should not be construed to make this an API promise. - // FIXME? Instead work extra hard to reject such input? - {"//busybox:latest" + sha256digest, "busybox" + sha256digest}, // Both tag and digest - {"//docker.io/library/busybox:latest", "busybox:latest"}, // All implied values explicitly specified - {"//UPPERCASEISINVALID", ""}, // Invalid input + // The docker/distribution API does not really support that (we can’t ask for an image with a specific + // tag and digest), so fail. This MAY be accepted in the future. + {"//busybox:latest" + sha256digest, ""}, // Both tag and digest + {"//docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}, // All implied values explicitly specified + {"//UPPERCASEISINVALID", ""}, // Invalid input } { ref, err := fn(c.input) if c.expected == "" { @@ -67,24 +65,17 @@ func testParseReference(t *testing.T, fn func(string) (types.ImageReference, err } } -// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time. -type refWithTagAndDigest struct{ reference.Canonical } - -func (ref refWithTagAndDigest) Tag() string { - return "notLatest" -} - // A common list of reference formats to test for the various ImageReference methods. var validReferenceTestCases = []struct{ input, dockerRef, stringWithinTransport string }{ - {"busybox:notlatest", "busybox:notlatest", "//busybox:notlatest"}, // Explicit tag - {"busybox" + sha256digest, "busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest - {"docker.io/library/busybox:latest", "busybox:latest", "//busybox:latest"}, // All implied values explicitly specified - {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified + {"busybox:notlatest", "docker.io/library/busybox:notlatest", "//busybox:notlatest"}, // Explicit tag + {"busybox" + sha256digest, "docker.io/library/busybox" + sha256digest, "//busybox" + sha256digest}, // Explicit digest + {"docker.io/library/busybox:latest", "docker.io/library/busybox:latest", "//busybox:latest"}, // All implied values explicitly specified + {"example.com/ns/foo:bar", "example.com/ns/foo:bar", "//example.com/ns/foo:bar"}, // All values explicitly specified } func TestNewReference(t *testing.T) { for _, c := range validReferenceTestCases { - parsed, err := reference.ParseNamed(c.input) + parsed, err := reference.ParseNormalizedNamed(c.input) require.NoError(t, err) ref, err := NewReference(parsed) require.NoError(t, err, c.input) @@ -94,18 +85,19 @@ func TestNewReference(t *testing.T) { } // Neither a tag nor digest - parsed, err := reference.ParseNamed("busybox") + parsed, err := reference.ParseNormalizedNamed("busybox") require.NoError(t, err) _, err = NewReference(parsed) assert.Error(t, err) // A github.com/distribution/reference value can have a tag and a digest at the same time! - parsed, err = reference.ParseNamed("busybox" + sha256digest) + parsed, err = reference.ParseNormalizedNamed("busybox:notlatest" + sha256digest) require.NoError(t, err) - refDigested, ok := parsed.(reference.Canonical) + _, ok := parsed.(reference.Canonical) require.True(t, ok) - tagDigestRef := refWithTagAndDigest{refDigested} - _, err = NewReference(tagDigestRef) + _, ok = parsed.(reference.NamedTagged) + require.True(t, ok) + _, err = NewReference(parsed) assert.Error(t, err) } @@ -196,7 +188,7 @@ func TestReferenceTagOrDigest(t *testing.T) { } // Invalid input - ref, err := reference.ParseNamed("busybox") + ref, err := reference.ParseNormalizedNamed("busybox") require.NoError(t, err) dockerRef := dockerReference{ref: ref} _, err = dockerRef.tagOrDigest() diff --git a/docker/lookaside.go b/docker/lookaside.go index e8f3a5be26..8896b758e0 100644 --- a/docker/lookaside.go +++ b/docker/lookaside.go @@ -64,7 +64,7 @@ func configuredSignatureStorageBase(ctx *types.SystemContext, ref dockerReferenc return nil, errors.Wrapf(err, "Invalid signature storage URL %s", topLevel) } // FIXME? Restrict to explicitly supported schemes? - repo := ref.ref.FullName() // Note that this is without a tag or digest. + repo := ref.ref.Name() // Note that this is without a tag or digest. if path.Clean(repo) != repo { // Coverage: This should not be reachable because /./ and /../ components are not valid in docker references return nil, errors.Errorf("Unexpected path elements in Docker reference %s for signature storage", ref.ref.String()) } diff --git a/docker/policyconfiguration/naming.go b/docker/policyconfiguration/naming.go index a40fa3807b..31bbb544c6 100644 --- a/docker/policyconfiguration/naming.go +++ b/docker/policyconfiguration/naming.go @@ -3,23 +3,22 @@ package policyconfiguration import ( "strings" - "github.com/pkg/errors" - "github.com/containers/image/docker/reference" + "github.com/pkg/errors" ) // DockerReferenceIdentity returns a string representation of the reference, suitable for policy lookup, // as a backend for ImageReference.PolicyConfigurationIdentity. // The reference must satisfy !reference.IsNameOnly(). func DockerReferenceIdentity(ref reference.Named) (string, error) { - res := ref.FullName() + res := ref.Name() tagged, isTagged := ref.(reference.NamedTagged) digested, isDigested := ref.(reference.Canonical) switch { - case isTagged && isDigested: // This should not happen, docker/reference.ParseNamed drops the tag. - return "", errors.Errorf("Unexpected Docker reference %s with both a name and a digest", ref.String()) + case isTagged && isDigested: // Note that this CAN actually happen. + return "", errors.Errorf("Unexpected Docker reference %s with both a name and a digest", reference.FamiliarString(ref)) case !isTagged && !isDigested: // This should not happen, the caller is expected to ensure !reference.IsNameOnly() - return "", errors.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", ref.String()) + return "", errors.Errorf("Internal inconsistency: Docker reference %s with neither a tag nor a digest", reference.FamiliarString(ref)) case isTagged: res = res + ":" + tagged.Tag() case isDigested: @@ -43,7 +42,7 @@ func DockerReferenceNamespaces(ref reference.Named) []string { // ref.FullName() == ref.Hostname() + "/" + ref.RemoteName(), so the last // iteration matches the host name (for any namespace). res := []string{} - name := ref.FullName() + name := ref.Name() for { res = append(res, name) diff --git a/docker/policyconfiguration/naming_test.go b/docker/policyconfiguration/naming_test.go index 0269db95cf..5998faa81f 100644 --- a/docker/policyconfiguration/naming_test.go +++ b/docker/policyconfiguration/naming_test.go @@ -1,11 +1,10 @@ package policyconfiguration import ( + "fmt" "strings" "testing" - "fmt" - "github.com/containers/image/docker/reference" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -35,14 +34,9 @@ func TestDockerReference(t *testing.T) { for inputSuffix, mappedSuffix := range map[string]string{ ":tag": ":tag", sha256Digest: sha256Digest, - // A github.com/distribution/reference value can have a tag and a digest at the same time! - // github.com/docker/reference handles that by dropping the tag. That is not obviously the - // right thing to do, but it is at least reasonable, so test that we keep behaving reasonably. - // This test case should not be construed to make this an API promise. - ":tag" + sha256Digest: sha256Digest, } { fullInput := inputName + inputSuffix - ref, err := reference.ParseNamed(fullInput) + ref, err := reference.ParseNormalizedNamed(fullInput) require.NoError(t, err, fullInput) identity, err := DockerReferenceIdentity(ref) @@ -62,30 +56,24 @@ func TestDockerReference(t *testing.T) { } } -// refWithTagAndDigest is a reference.NamedTagged and reference.Canonical at the same time. -type refWithTagAndDigest struct{ reference.Canonical } - -func (ref refWithTagAndDigest) Tag() string { - return "notLatest" -} - func TestDockerReferenceIdentity(t *testing.T) { // TestDockerReference above has tested the core of the functionality, this tests only the failure cases. // Neither a tag nor digest - parsed, err := reference.ParseNamed("busybox") + parsed, err := reference.ParseNormalizedNamed("busybox") require.NoError(t, err) id, err := DockerReferenceIdentity(parsed) assert.Equal(t, "", id) assert.Error(t, err) // A github.com/distribution/reference value can have a tag and a digest at the same time! - parsed, err = reference.ParseNamed("busybox@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + parsed, err = reference.ParseNormalizedNamed("busybox:notlatest@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") require.NoError(t, err) - refDigested, ok := parsed.(reference.Canonical) + _, ok := parsed.(reference.Canonical) + require.True(t, ok) + _, ok = parsed.(reference.NamedTagged) require.True(t, ok) - tagDigestRef := refWithTagAndDigest{refDigested} - id, err = DockerReferenceIdentity(tagDigestRef) + id, err = DockerReferenceIdentity(parsed) assert.Equal(t, "", id) assert.Error(t, err) } diff --git a/docker/reference/README.md b/docker/reference/README.md new file mode 100644 index 0000000000..53a88de826 --- /dev/null +++ b/docker/reference/README.md @@ -0,0 +1,2 @@ +This is a copy of github.com/docker/distribution/reference as of commit fb0bebc4b64e3881cc52a2478d749845ed76d2a8, +except that ParseAnyReferenceWithSet has been removed to drop the dependency on github.com/docker/distribution/digestset. \ No newline at end of file diff --git a/docker/reference/doc.go b/docker/reference/doc.go deleted file mode 100644 index a75ea749e5..0000000000 --- a/docker/reference/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// 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/helpers.go b/docker/reference/helpers.go new file mode 100644 index 0000000000..978df7eabb --- /dev/null +++ b/docker/reference/helpers.go @@ -0,0 +1,42 @@ +package reference + +import "path" + +// 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 +} + +// 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() +} + +// 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() +} + +// 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 +} diff --git a/docker/reference/normalize.go b/docker/reference/normalize.go new file mode 100644 index 0000000000..fcc436a395 --- /dev/null +++ b/docker/reference/normalize.go @@ -0,0 +1,152 @@ +package reference + +import ( + "errors" + "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, errors.New("invalid reference format: repository name must be lowercase") + } + + 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 +} + +// 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") { + 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 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/" + 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) +} diff --git a/docker/reference/normalize_test.go b/docker/reference/normalize_test.go new file mode 100644 index 0000000000..064ee749c0 --- /dev/null +++ b/docker/reference/normalize_test.go @@ -0,0 +1,573 @@ +package reference + +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", + "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", + + // 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", + } + 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", + } + + 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", + }, + } + + 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", + }, + } + + 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) + } + } +} diff --git a/docker/reference/reference.go b/docker/reference/reference.go index 38c30e2dbb..fd3510e9ee 100644 --- a/docker/reference/reference.go +++ b/docker/reference/reference.go @@ -1,41 +1,120 @@ +// Package reference provides a general type to represent any way of referencing images within the registry. +// Its main purpose is to abstract tags and digests (content-addressable hash). +// +// Grammar +// +// reference := name [ ":" tag ] [ "@" digest ] +// name := [domain '/'] path-component ['/' path-component]* +// domain := domain-component ['.' domain-component]* [':' port-number] +// 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 reference import ( - "regexp" + "errors" + "fmt" "strings" - // "opencontainers/go-digest" requires us to load the algorithms that we - // want to use into the binary (it calls .Available). - _ "crypto/sha256" - - distreference "github.com/docker/distribution/reference" "github.com/opencontainers/go-digest" - "github.com/pkg/errors" ) 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/" + // 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 { - // Name returns normalized repository name, like "ubuntu". + Reference 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 +} + +// Tagged is an object which has a tag +type Tagged interface { + Reference + Tag() string } // NamedTagged is an object including a name and tag. @@ -44,174 +123,311 @@ type NamedTagged interface { Tag() string } +// Digested is an object which has a digest +// in which it can be referenced by +type Digested interface { + Reference + Digest() digest.Digest +} + // Canonical reference is an object with a fully unique -// name including a name with hostname and digest +// name including a name with domain 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. +// 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. -func ParseNamed(s string) (Named, error) { - named, err := distreference.ParseNormalizedNamed(s) - if err != nil { - return nil, errors.Wrapf(err, "Error parsing reference: %q is not a valid repository/tag", s) +// 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 } - r, err := WithName(named.Name()) - if err != nil { - return nil, err + + if len(matches[1]) > NameTotalLengthMax { + return nil, ErrNameTooLong + } + + var repo repository + + nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1]) + if nameMatch != nil && 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 canonical, isCanonical := named.(distreference.Canonical); isCanonical { - r, err := distreference.WithDigest(r, canonical.Digest()) + if matches[3] != "" { + var err error + ref.digest, err = digest.Parse(matches[3]) if err != nil { return nil, err } - return &canonicalRef{namedRef{r}}, nil } - if tagged, isTagged := named.(distreference.NamedTagged); isTagged { - return WithTag(r, tagged.Tag()) + + r := getBestReferenceType(ref) + if r == nil { + return nil, ErrNameEmpty } + 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) +// 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 err := validateName(name); err != nil { - return nil, err + if named.String() != s { + return nil, ErrNameNotCanonical } - r, err := distreference.WithName(name) - if err != nil { - return nil, err + 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 } - return &namedRef{r}, nil + + 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) { - r, err := distreference.WithTag(name, tag) - if err != nil { - return nil, err + if !anchoredTagRegexp.MatchString(tag) { + return nil, ErrTagInvalidFormat } - return &taggedRef{namedRef{r}}, nil + 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 } -type namedRef struct { - distreference.Named +// 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 } -type taggedRef struct { - namedRef + +// TrimNamed removes any tag or digest from the named reference. +func TrimNamed(ref Named) Named { + domain, path := SplitHostname(ref) + return repository{ + domain: domain, + path: path, + } } -type canonicalRef struct { - namedRef + +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 } -func (r *namedRef) FullName() string { - hostname, remoteName := splitHostname(r.Name()) - return hostname + "/" + remoteName +type reference struct { + namedRepository + tag string + digest digest.Digest } -func (r *namedRef) Hostname() string { - hostname, _ := splitHostname(r.Name()) - return hostname + +func (r reference) String() string { + return r.Name() + ":" + r.tag + "@" + r.digest.String() } -func (r *namedRef) RemoteName() string { - _, remoteName := splitHostname(r.Name()) - return remoteName + +func (r reference) Tag() string { + return r.tag } -func (r *taggedRef) Tag() string { - return r.namedRef.Named.(distreference.NamedTagged).Tag() + +func (r reference) Digest() digest.Digest { + return r.digest } -func (r *canonicalRef) Digest() digest.Digest { - return digest.Digest(r.namedRef.Named.(distreference.Canonical).Digest()) + +type repository struct { + domain string + path string } -// 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 +func (r repository) String() string { + return r.Name() } -// IsNameOnly returns true if reference only contains a repo name. -func IsNameOnly(ref Named) bool { - if _, ok := ref.(NamedTagged); ok { - return false +func (r repository) Name() string { + if r.domain == "" { + return r.path } - if _, ok := ref.(Canonical); ok { - return false - } - return true + return r.domain + "/" + r.path } -// 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.Parse(idOrRef); err == nil { - return dgst, nil, nil - } - ref, err := ParseNamed(idOrRef) - return "", ref, err +func (r repository) Domain() string { + return r.domain } -// 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 +func (r repository) Path() string { + return r.path } -// 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 +type digestReference digest.Digest + +func (d digestReference) String() string { + return digest.Digest(d).String() } -var validHex = regexp.MustCompile(`^([a-f0-9]{64})$`) +func (d digestReference) Digest() digest.Digest { + return digest.Digest(d) +} -func validateID(id string) error { - if ok := validHex.MatchString(id); !ok { - return errors.Errorf("image ID %q is invalid", id) - } - return nil +type taggedReference struct { + namedRepository + tag string } -func validateName(name string) error { - if err := validateID(name); err == nil { - return errors.Errorf("Invalid repository name (%s), cannot specify 64-byte hexadecimal strings", name) - } - return nil +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 } diff --git a/docker/reference/reference_test.go b/docker/reference/reference_test.go index 79aa829fed..16b871f987 100644 --- a/docker/reference/reference_test.go +++ b/docker/reference/reference_test.go @@ -1,272 +1,659 @@ package reference import ( + _ "crypto/sha256" + _ "crypto/sha512" + "encoding/json" + "strconv" + "strings" "testing" - _ "crypto/sha256" + "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", - "thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev", +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/docker/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", + }, } - 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 _, 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() + } - for _, name := range invalidRepoNames { - _, err := ParseNamed(name) + _, err := WithName(testcase.input) if err == nil { - t.Fatalf("Expected invalid repo name for %q", name) + failf("no error parsing name. expected: %s", testcase.err) } } +} - for _, name := range validRepoNames { - _, err := ParseNamed(name) +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 { - t.Fatalf("Error parsing repo name %s, got: %q", name, err) + 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 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", +type serializationType struct { + Description string + Field Field +} - // Allow multiple hyphens as well. - "docker---rules/docker", +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() + } - //Username doc and image name docker being tested. - "doc/docker", + 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{} - // single character names are now allowed. - "d/docker", - "jess/t", + 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) + } - // 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) + continue + } else if testcase.err != nil { + failf("expected error unmarshalling: %v", testcase.err) } - } - invalidRepositoryNames := []string{ - // Disallow capital letters. - "docker/Docker", + if t.Description != testcase.description { + failf("wrong description, expected %q, got %q", testcase.description, t.Description) + } - // Only allow one slash. - "docker///docker", + ref := t.Field.Reference() - // Disallow 64-character hexadecimal. - "1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a", + 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) + } - // Disallow leading and trailing hyphens in namespace. - "-docker/docker", - "docker-/docker", - "-docker-/docker", + 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") + } - // Don't allow underscores everywhere (as opposed to hyphens). - "____/____", + 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") + } - "_docker/_docker", + t = serializationType{ + Description: testcase.description, + Field: AsField(ref), + } - // Disallow consecutive periods. - "dock..er/docker", - "dock_.er/docker", - "dock-.er/docker", + b2, err := json.Marshal(t) + if err != nil { + failf("error marshing serialization type: %v", err) + } - // No repository. - "docker/", + if string(b) != string(b2) { + failf("unexpected serialized value: expected %q, got %q", string(b), string(b2)) + } - //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) + // 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 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", - }, +func TestWithTag(t *testing.T) { + testcases := []struct { + name string + digest digest.Digest + tag string + combined string + }{ { - RemoteName: "nonlibrary/ubuntu", - NormalizedName: "nonlibrary/ubuntu", - FullName: "docker.io/nonlibrary/ubuntu", - AmbiguousName: "", - Hostname: "docker.io", + name: "test.com/foo", + tag: "tag", + combined: "test.com/foo:tag", }, { - RemoteName: "other/library", - NormalizedName: "other/library", - FullName: "docker.io/other/library", - AmbiguousName: "", - Hostname: "docker.io", + name: "foo", + tag: "tag2", + combined: "foo:tag2", }, { - 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", + name: "test.com:8000/foo", + tag: "tag4", + combined: "test.com:8000/foo:tag4", }, { - RemoteName: "privatebase", - NormalizedName: "127.0.0.1:8000/privatebase", - FullName: "127.0.0.1:8000/privatebase", - AmbiguousName: "", - Hostname: "127.0.0.1:8000", + name: "test.com:8000/foo", + tag: "TAG5", + combined: "test.com:8000/foo:TAG5", }, { - RemoteName: "private/moonbase", - NormalizedName: "example.com/private/moonbase", - FullName: "example.com/private/moonbase", - AmbiguousName: "", - Hostname: "example.com", + 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 + }{ { - RemoteName: "privatebase", - NormalizedName: "example.com/privatebase", - FullName: "example.com/privatebase", - AmbiguousName: "", - Hostname: "example.com", + name: "test.com/foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "test.com/foo@sha256:1234567890098765432112345667890098765", }, { - RemoteName: "private/moonbase", - NormalizedName: "example.com:8000/private/moonbase", - FullName: "example.com:8000/private/moonbase", - AmbiguousName: "", - Hostname: "example.com:8000", + name: "foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "foo@sha256:1234567890098765432112345667890098765", }, { - RemoteName: "privatebasee", - NormalizedName: "example.com:8000/privatebasee", - FullName: "example.com:8000/privatebasee", - AmbiguousName: "", - Hostname: "example.com:8000", + name: "test.com:8000/foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765", }, { - 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", + name: "test.com:8000/foo", + digest: "sha256:1234567890098765432112345667890098765", + tag: "latest", + combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765", }, } - - for _, tcase := range tcases { - refStrings := []string{tcase.NormalizedName, tcase.FullName} - if tcase.AmbiguousName != "" { - refStrings = append(refStrings, tcase.AmbiguousName) + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.name)+": "+format, v...) + t.Fail() } - 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) + 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 { - t.Fatal(err) + failf("error adding tag") } - refs = append(refs, named) + named = tagged } - - 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) - } - + 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 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 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() + } -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") + 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 succeded: 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) + } } } diff --git a/docker/reference/regexp.go b/docker/reference/regexp.go new file mode 100644 index 0000000000..405e995db9 --- /dev/null +++ b/docker/reference/regexp.go @@ -0,0 +1,143 @@ +package reference + +import "regexp" + +var ( + // alphaNumericRegexp defines the alpha numeric atom, typically a + // component of names. This only allows lower case characters and digits. + alphaNumericRegexp = match(`[a-z0-9]+`) + + // separatorRegexp defines the separators allowed to be embedded in name + // components. This allow one period, one or two underscore and multiple + // dashes. + separatorRegexp = match(`(?:[._]|__|[-]*)`) + + // nameComponentRegexp restricts registry path component names to start + // with at least one letter or number, with following parts able to be + // separated by one period, one or two underscore and multiple dashes. + nameComponentRegexp = expression( + alphaNumericRegexp, + optional(repeated(separatorRegexp, alphaNumericRegexp))) + + // domainComponentRegexp restricts the registry domain component of a + // repository name to start with a component as defined by domainRegexp + // and followed by an optional port. + domainComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-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 = expression( + domainComponentRegexp, + optional(repeated(literal(`.`), domainComponentRegexp)), + optional(literal(`:`), match(`[0-9]+`))) + + // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. + TagRegexp = match(`[\w][\w.-]{0,127}`) + + // anchoredTagRegexp matches valid tag names, anchored at the start and + // end of the matched string. + anchoredTagRegexp = anchored(TagRegexp) + + // DigestRegexp matches valid digests. + DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`) + + // anchoredDigestRegexp matches valid digests, anchored at the start and + // end of the matched string. + anchoredDigestRegexp = anchored(DigestRegexp) + + // 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 = expression( + optional(domainRegexp, literal(`/`)), + nameComponentRegexp, + optional(repeated(literal(`/`), nameComponentRegexp))) + + // anchoredNameRegexp is used to parse a name value, capturing the + // domain and trailing components. + anchoredNameRegexp = anchored( + optional(capture(domainRegexp), literal(`/`)), + capture(nameComponentRegexp, + optional(repeated(literal(`/`), nameComponentRegexp)))) + + // ReferenceRegexp is the full supported format of a reference. The regexp + // is anchored and has capturing groups for name, tag, and digest + // components. + ReferenceRegexp = anchored(capture(NameRegexp), + optional(literal(":"), capture(TagRegexp)), + optional(literal("@"), capture(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. + IdentifierRegexp = match(`([a-f0-9]{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 = match(`([a-f0-9]{6,64})`) + + // anchoredIdentifierRegexp is used to check or match an + // identifier value, anchored at start and end of string. + anchoredIdentifierRegexp = anchored(IdentifierRegexp) + + // anchoredShortIdentifierRegexp is used to check if a value + // is a possible identifier prefix, anchored at start and end + // of string. + anchoredShortIdentifierRegexp = anchored(ShortIdentifierRegexp) +) + +// match compiles the string to a regular expression. +var match = regexp.MustCompile + +// literal compiles s into a literal regular expression, escaping any regexp +// reserved characters. +func literal(s string) *regexp.Regexp { + re := match(regexp.QuoteMeta(s)) + + if _, complete := re.LiteralPrefix(); !complete { + panic("must be a literal") + } + + return re +} + +// expression defines a full expression, where each regular expression must +// follow the previous. +func expression(res ...*regexp.Regexp) *regexp.Regexp { + var s string + for _, re := range res { + s += re.String() + } + + return match(s) +} + +// optional wraps the expression in a non-capturing group and makes the +// production optional. +func optional(res ...*regexp.Regexp) *regexp.Regexp { + return match(group(expression(res...)).String() + `?`) +} + +// repeated wraps the regexp in a non-capturing group to get one or more +// matches. +func repeated(res ...*regexp.Regexp) *regexp.Regexp { + return match(group(expression(res...)).String() + `+`) +} + +// group wraps the regexp in a non-capturing group. +func group(res ...*regexp.Regexp) *regexp.Regexp { + return match(`(?:` + expression(res...).String() + `)`) +} + +// capture wraps the expression in a capturing group. +func capture(res ...*regexp.Regexp) *regexp.Regexp { + return match(`(` + expression(res...).String() + `)`) +} + +// anchored anchors the regular expression by adding start and end delimiters. +func anchored(res ...*regexp.Regexp) *regexp.Regexp { + return match(`^` + expression(res...).String() + `$`) +} diff --git a/docker/reference/regexp_test.go b/docker/reference/regexp_test.go new file mode 100644 index 0000000000..c21263992f --- /dev/null +++ b/docker/reference/regexp_test.go @@ -0,0 +1,553 @@ +package reference + +import ( + "regexp" + "strings" + "testing" +) + +type regexpMatch struct { + input string + match bool + subs []string +} + +func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) { + matches := r.FindStringSubmatch(m.input) + if m.match && matches != nil { + if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input { + t.Fatalf("Bad match result %#v for %q", matches, m.input) + } + if len(matches) < (len(m.subs) + 1) { + t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input) + } + for i := range m.subs { + if m.subs[i] != matches[i+1] { + t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input) + } + } + } else if m.match { + t.Errorf("Expected match for %q", m.input) + } else if matches != nil { + t.Errorf("Unexpected match for %q", m.input) + } +} + +func 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, + }, + } + 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, + }, + } + + shortCases := []regexpMatch{ + { + input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", + match: true, + }, + { + input: "7EC43B381E5AEFE6E04EFB0B3F0693FF2A4A50652D64AEC573905F2DB5889A1C", + match: false, + }, + { + input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf", + match: true, + }, + { + input: "sha256:da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf9821", + match: false, + }, + { + input: "da304e823d8ca2b9d863a3c897baeb852ba21ea9a9f1414736394ae7fcaf98218482", + match: false, + }, + { + input: "da304", + match: false, + }, + { + input: "da304e", + match: true, + }, + } + + for i := range fullCases { + checkRegexp(t, anchoredIdentifierRegexp, fullCases[i]) + } + + for i := range shortCases { + checkRegexp(t, anchoredShortIdentifierRegexp, shortCases[i]) + } +} diff --git a/image/docker_schema1.go b/image/docker_schema1.go index dce81a14d1..7d6de1a608 100644 --- a/image/docker_schema1.go +++ b/image/docker_schema1.go @@ -72,7 +72,7 @@ func manifestSchema1FromManifest(manifest []byte) (genericManifest, error) { func manifestSchema1FromComponents(ref reference.Named, fsLayers []fsLayersSchema1, history []historySchema1, architecture string) genericManifest { var name, tag string if ref != nil { // Well, what to do if it _is_ nil? Most consumers actually don't use these fields nowadays, so we might as well try not supplying them. - name = ref.RemoteName() + name = reference.Path(ref) if tagged, ok := ref.(reference.NamedTagged); ok { tag = tagged.Tag() } diff --git a/image/docker_schema2_test.go b/image/docker_schema2_test.go index 52ef308929..473e51e246 100644 --- a/image/docker_schema2_test.go +++ b/image/docker_schema2_test.go @@ -9,13 +9,12 @@ import ( "testing" "time" - "github.com/pkg/errors" - "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -326,7 +325,7 @@ func newSchema2ImageSource(t *testing.T, dockerRef string) *schema2ImageSource { realConfigJSON, err := ioutil.ReadFile("fixtures/schema2-config.json") require.NoError(t, err) - ref, err := reference.ParseNamed(dockerRef) + ref, err := reference.ParseNormalizedNamed(dockerRef) require.NoError(t, err) return &schema2ImageSource{ diff --git a/image/oci_test.go b/image/oci_test.go index ac74fbec13..c51b3ae774 100644 --- a/image/oci_test.go +++ b/image/oci_test.go @@ -9,13 +9,12 @@ import ( "testing" "time" - "github.com/pkg/errors" - "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -260,7 +259,7 @@ func newOCI1ImageSource(t *testing.T, dockerRef string) *oci1ImageSource { realConfigJSON, err := ioutil.ReadFile("fixtures/oci1-config.json") require.NoError(t, err) - ref, err := reference.ParseNamed(dockerRef) + ref, err := reference.ParseNormalizedNamed(dockerRef) require.NoError(t, err) return &oci1ImageSource{ diff --git a/image/unparsed.go b/image/unparsed.go index 1e1ee0b528..0feb1101c5 100644 --- a/image/unparsed.go +++ b/image/unparsed.go @@ -4,6 +4,7 @@ import ( "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" "github.com/containers/image/types" + "github.com/opencontainers/go-digest" "github.com/pkg/errors" ) @@ -52,7 +53,7 @@ func (i *UnparsedImage) Manifest() ([]byte, string, error) { ref := i.Reference().DockerReference() if ref != nil { if canonical, ok := ref.(reference.Canonical); ok { - digest := canonical.Digest() + digest := digest.Digest(canonical.Digest()) matches, err := manifest.MatchesDigest(m, digest) if err != nil { return nil, "", errors.Wrap(err, "Error computing manifest digest") diff --git a/openshift/openshift.go b/openshift/openshift.go index 1fc0e24c34..c1de090d63 100644 --- a/openshift/openshift.go +++ b/openshift/openshift.go @@ -13,6 +13,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/containers/image/docker" + "github.com/containers/image/docker/reference" "github.com/containers/image/manifest" "github.com/containers/image/types" "github.com/containers/image/version" @@ -153,7 +154,7 @@ func (c *openshiftClient) convertDockerImageReference(ref string) (string, error if len(parts) != 2 { return "", errors.Errorf("Invalid format of docker reference %s: missing '/'", ref) } - return c.ref.dockerReference.Hostname() + "/" + parts[1], nil + return reference.Domain(c.ref.dockerReference) + "/" + parts[1], nil } type openshiftImageSource struct { @@ -305,7 +306,7 @@ func newImageDestination(ctx *types.SystemContext, ref openshiftReference) (type // FIXME: Should this always use a digest, not a tag? Uploading to Docker by tag requires the tag _inside_ the manifest to match, // i.e. a single signed image cannot be available under multiple tags. But with types.ImageDestination, we don't know // the manifest digest at this point. - dockerRefString := fmt.Sprintf("//%s/%s/%s:%s", client.ref.dockerReference.Hostname(), client.ref.namespace, client.ref.stream, client.ref.dockerReference.Tag()) + dockerRefString := fmt.Sprintf("//%s/%s/%s:%s", reference.Domain(client.ref.dockerReference), client.ref.namespace, client.ref.stream, client.ref.dockerReference.Tag()) dockerRef, err := docker.ParseReference(dockerRefString) if err != nil { return nil, err diff --git a/openshift/openshift_transport.go b/openshift/openshift_transport.go index b37fb2c21d..119385bbc3 100644 --- a/openshift/openshift_transport.go +++ b/openshift/openshift_transport.go @@ -51,7 +51,7 @@ type openshiftReference struct { // ParseReference converts a string, which should not start with the ImageTransport.Name prefix, into an OpenShift ImageReference. func ParseReference(ref string) (types.ImageReference, error) { - r, err := reference.ParseNamed(ref) + r, err := reference.ParseNormalizedNamed(ref) if err != nil { return nil, errors.Wrapf(err, "failed to parse image reference %q", ref) } @@ -64,10 +64,10 @@ func ParseReference(ref string) (types.ImageReference, error) { // NewReference returns an OpenShift reference for a reference.NamedTagged func NewReference(dockerRef reference.NamedTagged) (types.ImageReference, error) { - r := strings.SplitN(dockerRef.RemoteName(), "/", 3) + r := strings.SplitN(reference.Path(dockerRef), "/", 3) if len(r) != 2 { return nil, errors.Errorf("invalid image reference: %s, expected format: 'hostname/namespace/stream:tag'", - dockerRef.String()) + reference.FamiliarString(dockerRef)) } return openshiftReference{ namespace: r[0], @@ -86,7 +86,7 @@ func (ref openshiftReference) Transport() types.ImageTransport { // e.g. default attribute values omitted by the user may be filled in in the return value, or vice versa. // WARNING: Do not use the return value in the UI to describe an image, it does not contain the Transport().Name() prefix. func (ref openshiftReference) StringWithinTransport() string { - return ref.dockerReference.String() + return reference.FamiliarString(ref.dockerReference) } // DockerReference returns a Docker reference associated with this reference diff --git a/openshift/openshift_transport_test.go b/openshift/openshift_transport_test.go index 46904ff369..5c589c1923 100644 --- a/openshift/openshift_transport_test.go +++ b/openshift/openshift_transport_test.go @@ -40,14 +40,14 @@ func TestTransportValidatePolicyConfigurationScope(t *testing.T) { func TestNewReference(t *testing.T) { // too many ns - r, err := reference.ParseNamed("registry.example.com/ns1/ns2/ns3/stream:tag") + r, err := reference.ParseNormalizedNamed("registry.example.com/ns1/ns2/ns3/stream:tag") require.NoError(t, err) tagged, ok := r.(reference.NamedTagged) require.True(t, ok) _, err = NewReference(tagged) assert.Error(t, err) - r, err = reference.ParseNamed("registry.example.com/ns/stream:tag") + r, err = reference.ParseNormalizedNamed("registry.example.com/ns/stream:tag") require.NoError(t, err) tagged, ok = r.(reference.NamedTagged) require.True(t, ok) @@ -64,7 +64,7 @@ func TestParseReference(t *testing.T) { assert.Equal(t, "ns", osRef.namespace) assert.Equal(t, "stream", osRef.stream) assert.Equal(t, "notlatest", osRef.dockerReference.Tag()) - assert.Equal(t, "registry.example.com:8443", osRef.dockerReference.Hostname()) + assert.Equal(t, "registry.example.com:8443", reference.Domain(osRef.dockerReference)) // Components creating an invalid Docker Reference name _, err = ParseReference("registry.example.com/ns/UPPERCASEISINVALID:notlatest") diff --git a/signature/docker.go b/signature/docker.go index 901a225a29..16eb3f7993 100644 --- a/signature/docker.go +++ b/signature/docker.go @@ -25,7 +25,7 @@ func SignDockerManifest(m []byte, dockerReference string, mech SigningMechanism, // using mech. func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byte, expectedDockerReference string, mech SigningMechanism, expectedKeyIdentity string) (*Signature, error) { - expectedRef, err := reference.ParseNamed(expectedDockerReference) + expectedRef, err := reference.ParseNormalizedNamed(expectedDockerReference) if err != nil { return nil, err } @@ -37,7 +37,7 @@ func VerifyDockerManifestSignature(unverifiedSignature, unverifiedManifest []byt return nil }, validateSignedDockerReference: func(signedDockerReference string) error { - signedRef, err := reference.ParseNamed(signedDockerReference) + signedRef, err := reference.ParseNormalizedNamed(signedDockerReference) if err != nil { return InvalidSignatureError{msg: fmt.Sprintf("Invalid docker reference %s in signature", signedDockerReference)} } diff --git a/signature/policy_config.go b/signature/policy_config.go index e4525795dc..ace24fec88 100644 --- a/signature/policy_config.go +++ b/signature/policy_config.go @@ -19,11 +19,10 @@ import ( "io/ioutil" "path/filepath" - "github.com/pkg/errors" - "github.com/containers/image/docker/reference" "github.com/containers/image/transports" "github.com/containers/image/types" + "github.com/pkg/errors" ) // systemDefaultPolicyPath is the policy path used for DefaultPolicy(). @@ -634,7 +633,7 @@ func (prm *prmMatchRepository) UnmarshalJSON(data []byte) error { // newPRMExactReference is NewPRMExactReference, except it resturns the private type. func newPRMExactReference(dockerReference string) (*prmExactReference, error) { - ref, err := reference.ParseNamed(dockerReference) + ref, err := reference.ParseNormalizedNamed(dockerReference) if err != nil { return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerReference %s: %s", dockerReference, err.Error())) } @@ -686,7 +685,7 @@ func (prm *prmExactReference) UnmarshalJSON(data []byte) error { // newPRMExactRepository is NewPRMExactRepository, except it resturns the private type. func newPRMExactRepository(dockerRepository string) (*prmExactRepository, error) { - if _, err := reference.ParseNamed(dockerRepository); err != nil { + if _, err := reference.ParseNormalizedNamed(dockerRepository); err != nil { return nil, InvalidPolicyFormatError(fmt.Sprintf("Invalid format of dockerRepository %s: %s", dockerRepository, err.Error())) } return &prmExactRepository{ diff --git a/signature/policy_eval_signedby_test.go b/signature/policy_eval_signedby_test.go index d21ee9c17f..19086fcf5b 100644 --- a/signature/policy_eval_signedby_test.go +++ b/signature/policy_eval_signedby_test.go @@ -17,7 +17,7 @@ import ( // dirImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference. // The caller must call .Close() on the returned UnparsedImage. func dirImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage { - ref, err := reference.ParseNamed(dockerReference) + ref, err := reference.ParseNormalizedNamed(dockerReference) require.NoError(t, err) return dirImageMockWithRef(t, dir, refImageReferenceMock{ref}) } diff --git a/signature/policy_eval_test.go b/signature/policy_eval_test.go index c0bfe1a39a..785d7900b5 100644 --- a/signature/policy_eval_test.go +++ b/signature/policy_eval_test.go @@ -159,7 +159,7 @@ func TestPolicyContextRequirementsForImageRef(t *testing.T) { expected = policy.Default } - ref, err := reference.ParseNamed(c.input) + ref, err := reference.ParseNormalizedNamed(c.input) require.NoError(t, err) reqs := pc.requirementsForImageRef(pcImageReferenceMock{c.inputTransport, ref}) comment := fmt.Sprintf("case %s:%s: %#v", c.inputTransport, c.input, reqs[0]) @@ -174,7 +174,7 @@ func TestPolicyContextRequirementsForImageRef(t *testing.T) { // pcImageMock returns a types.UnparsedImage for a directory, claiming a specified dockerReference and implementing PolicyConfigurationIdentity/PolicyConfigurationNamespaces. // The caller must call .Close() on the returned Image. func pcImageMock(t *testing.T, dir, dockerReference string) types.UnparsedImage { - ref, err := reference.ParseNamed(dockerReference) + ref, err := reference.ParseNormalizedNamed(dockerReference) require.NoError(t, err) return dirImageMockWithRef(t, dir, pcImageReferenceMock{"docker", ref}) } diff --git a/signature/policy_reference_match.go b/signature/policy_reference_match.go index ced51e6e07..a8dad67701 100644 --- a/signature/policy_reference_match.go +++ b/signature/policy_reference_match.go @@ -17,7 +17,7 @@ func parseImageAndDockerReference(image types.UnparsedImage, s2 string) (referen return nil, nil, PolicyRequirementError(fmt.Sprintf("Docker reference match attempted on image %s with no known Docker reference identity", transports.ImageName(image.Reference()))) } - r2, err := reference.ParseNamed(s2) + r2, err := reference.ParseNormalizedNamed(s2) if err != nil { return nil, nil, err } @@ -69,11 +69,11 @@ func (prm *prmMatchRepository) matchesDockerReference(image types.UnparsedImage, // parseDockerReferences converts two reference strings into parsed entities, failing on any error func parseDockerReferences(s1, s2 string) (reference.Named, reference.Named, error) { - r1, err := reference.ParseNamed(s1) + r1, err := reference.ParseNormalizedNamed(s1) if err != nil { return nil, nil, err } - r2, err := reference.ParseNamed(s2) + r2, err := reference.ParseNormalizedNamed(s2) if err != nil { return nil, nil, err } diff --git a/signature/policy_reference_match_test.go b/signature/policy_reference_match_test.go index 2554182168..295e8339a0 100644 --- a/signature/policy_reference_match_test.go +++ b/signature/policy_reference_match_test.go @@ -6,7 +6,6 @@ import ( "github.com/containers/image/docker/reference" "github.com/containers/image/types" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,12 +25,12 @@ func TestParseImageAndDockerReference(t *testing.T) { bad2 = "" ) // Success - ref, err := reference.ParseNamed(ok1) + ref, err := reference.ParseNormalizedNamed(ok1) require.NoError(t, err) r1, r2, err := parseImageAndDockerReference(refImageMock{ref}, ok2) require.NoError(t, err) - assert.Equal(t, ok1, r1.String()) - assert.Equal(t, ok2, r2.String()) + assert.Equal(t, ok1, reference.FamiliarString(r1)) + assert.Equal(t, ok2, reference.FamiliarString(r2)) // Unidentified images are rejected. _, _, err = parseImageAndDockerReference(refImageMock{nil}, ok2) @@ -44,7 +43,7 @@ func TestParseImageAndDockerReference(t *testing.T) { {ok1, bad2}, {bad1, bad2}, } { - ref, err := reference.ParseNamed(refs[0]) + ref, err := reference.ParseNormalizedNamed(refs[0]) if err == nil { _, _, err := parseImageAndDockerReference(refImageMock{ref}, refs[1]) assert.Error(t, err) @@ -72,7 +71,7 @@ func (ref refImageMock) Signatures() ([][]byte, error) { type refImageReferenceMock struct{ reference.Named } func (ref refImageReferenceMock) Transport() types.ImageTransport { - // We use this in error messages, so sadly we must return something. But right now we do so only when DockerReference is nil, so restrict to that. + // We use this in error messages, so sady we must return something. But right now we do so only when DockerReference is nil, so restrict to that. if ref.Named == nil { return nameImageTransportMock("== Transport mock") } @@ -148,14 +147,12 @@ var prmExactMatchTestTable = []prmSymmetricTableTest{ {"busybox", "busybox:latest", false}, {"busybox", "busybox" + digestSuffix, false}, {"busybox", "busybox", false}, - // References with both tags and digests: `reference.WithName` essentially drops the tag. - // This is not _particularly_ desirable but it is the semantics used throughout containers/image; at least, with the digest it is clear which image the reference means, - // even if the tag may reflect a different user intent. + // References with both tags and digests: We match them exactly (requiring BOTH to match) // NOTE: Again, this is not documented behavior; the recommendation is to sign tags, not digests, and then tag-and-digest references won’t match the signed identity. {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true}, {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false}, - {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true}, // Ugly. Do not rely on this. - {"busybox:latest" + digestSuffix, "busybox" + digestSuffix, true}, // Ugly. Do not rely on this. + {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, false}, + {"busybox:latest" + digestSuffix, "busybox" + digestSuffix, false}, {"busybox:latest" + digestSuffix, "busybox:latest", false}, // Invalid format {"UPPERCASE_IS_INVALID_IN_DOCKER_REFERENCES", "busybox:latest", false}, @@ -194,7 +191,7 @@ var prmRepositoryMatchTestTable = []prmSymmetricTableTest{ {"hostname/library/busybox:latest", "busybox:notlatest", false}, {"busybox:latest", fullRHELRef, false}, {"busybox" + digestSuffix, "notbusybox" + digestSuffix, false}, - // References with both tags and digests: `reference.WithName` essentially drops the tag, and we ignore both anyway. + // References with both tags and digests: We ignore both anyway. {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffix, true}, {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true}, {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffix, true}, @@ -209,8 +206,8 @@ var prmRepositoryMatchTestTable = []prmSymmetricTableTest{ func testImageAndSig(t *testing.T, prm PolicyReferenceMatch, imageRef, sigRef string, result bool) { // This assumes that all ways to obtain a reference.Named perform equivalent validation, - // and therefore values refused by reference.ParseNamed can not happen in practice. - parsedImageRef, err := reference.ParseNamed(imageRef) + // and therefore values refused by reference.ParseNormalizedNamed can not happen in practice. + parsedImageRef, err := reference.ParseNormalizedNamed(imageRef) if err != nil { return } @@ -272,14 +269,12 @@ func TestPMMMatchRepoDigestOrExactMatchesDockerReference(t *testing.T) { // Digest references accept any signature with matching repository. {"busybox" + digestSuffix, "busybox:latest", true}, {"busybox" + digestSuffix, "busybox" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.) - // References with both tags and digests: `reference.WithName` essentially drops the tag. - // This is not _particularly_ desirable but it is the semantics used throughout containers/image; at least, with the digest it is clear which image the reference means, - // even if the tag may reflect a different user intent. - {"busybox:latest" + digestSuffix, "busybox:latest", true}, - {"busybox:latest" + digestSuffix, "busybox:notlatest", true}, + // References with both tags and digests: We match them exactly (requiring BOTH to match). + {"busybox:latest" + digestSuffix, "busybox:latest", false}, + {"busybox:latest" + digestSuffix, "busybox:notlatest", false}, {"busybox:latest", "busybox:latest" + digestSuffix, false}, - {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, true}, // Even this is accepted here. (This could more reasonably happen with two different digest algorithms.) - {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, true}, // Ugly. Do not rely on this. + {"busybox:latest" + digestSuffix, "busybox:latest" + digestSuffixOther, false}, + {"busybox:latest" + digestSuffix, "busybox:notlatest" + digestSuffixOther, false}, } { testImageAndSig(t, prm, test.imageRef, test.sigRef, test.result) } @@ -307,8 +302,8 @@ func TestParseDockerReferences(t *testing.T) { // Success r1, r2, err := parseDockerReferences(ok1, ok2) require.NoError(t, err) - assert.Equal(t, ok1, r1.String()) - assert.Equal(t, ok2, r2.String()) + assert.Equal(t, ok1, reference.FamiliarString(r1)) + assert.Equal(t, ok2, reference.FamiliarString(r2)) // Failures for _, refs := range [][]string{ diff --git a/storage/storage_reference.go b/storage/storage_reference.go index 13413df1bc..bee753f45f 100644 --- a/storage/storage_reference.go +++ b/storage/storage_reference.go @@ -87,7 +87,7 @@ func (s storageReference) PolicyConfigurationNamespaces() []string { // The reference without the ID is also a valid namespace. namespaces = append(namespaces, storeSpec+s.reference) } - components := strings.Split(s.name.FullName(), "/") + components := strings.Split(s.name.Name(), "/") for len(components) > 0 { namespaces = append(namespaces, storeSpec+strings.Join(components, "/")) components = components[:len(components)-1] diff --git a/storage/storage_reference_test.go b/storage/storage_reference_test.go index 687d1005dc..ee4613414d 100644 --- a/storage/storage_reference_test.go +++ b/storage/storage_reference_test.go @@ -23,7 +23,7 @@ func TestStorageReferenceDockerReference(t *testing.T) { require.NoError(t, err) dr := ref.DockerReference() require.NotNil(t, dr) - assert.Equal(t, "busybox:latest", dr.String()) + assert.Equal(t, "docker.io/library/busybox:latest", dr.String()) ref, err = Transport.ParseReference("@" + sha256digestHex) require.NoError(t, err) diff --git a/storage/storage_transport.go b/storage/storage_transport.go index 661df1038a..78c7ef651a 100644 --- a/storage/storage_transport.go +++ b/storage/storage_transport.go @@ -83,14 +83,14 @@ func (s storageTransport) ParseStoreReference(store storage.Store, ref string) ( refInfo := strings.SplitN(ref, "@", 2) if len(refInfo) == 1 { // A name. - name, err = reference.ParseNamed(refInfo[0]) + name, err = reference.ParseNormalizedNamed(refInfo[0]) if err != nil { return nil, err } } else if len(refInfo) == 2 { // An ID, possibly preceded by a name. if refInfo[0] != "" { - name, err = reference.ParseNamed(refInfo[0]) + name, err = reference.ParseNormalizedNamed(refInfo[0]) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func (s storageTransport) ParseStoreReference(store storage.Store, ref string) ( } refname := "" if name != nil { - name = reference.WithDefaultTag(name) + name = reference.TagNameOnly(name) refname = verboseName(name) } if refname == "" { @@ -257,12 +257,12 @@ func (s storageTransport) ValidatePolicyConfigurationScope(scope string) error { // that are just bare IDs. scopeInfo := strings.SplitN(scope, "@", 2) if len(scopeInfo) == 1 && scopeInfo[0] != "" { - _, err := reference.ParseNamed(scopeInfo[0]) + _, err := reference.ParseNormalizedNamed(scopeInfo[0]) if err != nil { return err } } else if len(scopeInfo) == 2 && scopeInfo[0] != "" && scopeInfo[1] != "" { - _, err := reference.ParseNamed(scopeInfo[0]) + _, err := reference.ParseNormalizedNamed(scopeInfo[0]) if err != nil { return err } @@ -277,10 +277,10 @@ func (s storageTransport) ValidatePolicyConfigurationScope(scope string) error { } func verboseName(name reference.Named) string { - name = reference.WithDefaultTag(name) + name = reference.TagNameOnly(name) tag := "" if tagged, ok := name.(reference.NamedTagged); ok { tag = tagged.Tag() } - return name.FullName() + ":" + tag + return name.Name() + ":" + tag } diff --git a/storage/storage_transport_test.go b/storage/storage_transport_test.go index 2ca7a657e2..3fddff5ccc 100644 --- a/storage/storage_transport_test.go +++ b/storage/storage_transport_test.go @@ -54,7 +54,7 @@ func TestTransportParseStoreReference(t *testing.T) { if c.expectedRef == "" { assert.Nil(t, storageRef.name, c.input) } else { - dockerRef, err := reference.ParseNamed(c.expectedRef) + dockerRef, err := reference.ParseNormalizedNamed(c.expectedRef) require.NoError(t, err) require.NotNil(t, storageRef.name, c.input) assert.Equal(t, dockerRef.String(), storageRef.name.String()) diff --git a/types/types.go b/types/types.go index 517388a047..2154f306f7 100644 --- a/types/types.go +++ b/types/types.go @@ -4,10 +4,9 @@ import ( "io" "time" - "github.com/pkg/errors" - "github.com/containers/image/docker/reference" "github.com/opencontainers/go-digest" + "github.com/pkg/errors" ) // ImageTransport is a top-level namespace for ways to to store/load an image.