From 2c0d852000e303f48a0a982af01673ba9dd1ff76 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 9 Oct 2015 17:09:54 -0700 Subject: [PATCH] Add WithTag and WithDigest combinator functions These functions allow a Named type to be combined with a tag or a digest. WithTag will replace the ImageReference function in github.com/docker/docker/utils as the Docker Engine transitions to the reference package. Signed-off-by: Aaron Lehmann --- reference/reference.go | 30 +++++++++++++ reference/reference_test.go | 84 +++++++++++++++++++++++++++++++++++++ reference/regexp.go | 9 +++- 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/reference/reference.go b/reference/reference.go index 01e7fca7..d115a946 100644 --- a/reference/reference.go +++ b/reference/reference.go @@ -43,6 +43,12 @@ 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") + // ErrNameEmpty is returned for empty, invalid repository names. ErrNameEmpty = errors.New("repository name must have at least one component") @@ -182,6 +188,30 @@ func ParseNamed(name string) (Named, error) { return repository(name), 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) (Tagged, error) { + if !anchoredNameRegexp.MatchString(tag) { + return nil, ErrTagInvalidFormat + } + return taggedReference{ + name: name.Name(), + tag: tag, + }, nil +} + +// WithDigest combines the name from "name" and the digest from "digest" to form +// a reference incorporating both the name and the digest. +func WithDigest(name Named, digest digest.Digest) (Digested, error) { + if !anchoredDigestRegexp.MatchString(digest.String()) { + return nil, ErrDigestInvalidFormat + } + return canonicalReference{ + name: name.Name(), + digest: digest, + }, nil +} + func getBestReferenceType(ref reference) Reference { if ref.name == "" { // Allow digest only references diff --git a/reference/reference_test.go b/reference/reference_test.go index 3207f525..d47abbf8 100644 --- a/reference/reference_test.go +++ b/reference/reference_test.go @@ -395,3 +395,87 @@ func TestSerialization(t *testing.T) { } } + +func TestWithTag(t *testing.T) { + testcases := []struct { + name string + tag string + combined string + }{ + { + name: "test.com/foo", + tag: "tag", + combined: "test.com/foo:tag", + }, + { + name: "foo", + tag: "tag2", + combined: "foo:tag2", + }, + { + name: "test.com:8000/foo", + tag: "tag4", + combined: "test.com:8000/foo:tag4", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.name)+": "+format, v...) + t.Fail() + } + + named, err := ParseNamed(testcase.name) + if err != nil { + failf("error parsing name: %s", err) + } + 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 + combined string + }{ + { + name: "test.com/foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "test.com/foo@sha256:1234567890098765432112345667890098765", + }, + { + name: "foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "foo@sha256:1234567890098765432112345667890098765", + }, + { + name: "test.com:8000/foo", + digest: "sha256:1234567890098765432112345667890098765", + combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765", + }, + } + for _, testcase := range testcases { + failf := func(format string, v ...interface{}) { + t.Logf(strconv.Quote(testcase.name)+": "+format, v...) + t.Fail() + } + + named, err := ParseNamed(testcase.name) + if err != nil { + failf("error parsing name: %s", err) + } + 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) + } + } +} diff --git a/reference/regexp.go b/reference/regexp.go index 579d5cda..06ca8db3 100644 --- a/reference/regexp.go +++ b/reference/regexp.go @@ -28,6 +28,13 @@ var ( // end of the matched string. anchoredTagRegexp = regexp.MustCompile(`^` + TagRegexp.String() + `$`) + // DigestRegexp matches valid digests. + DigestRegexp = regexp.MustCompile(`[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 = regexp.MustCompile(`^` + DigestRegexp.String() + `$`) + // NameRegexp is the format for the name component of references. The // regexp has capturing groups for the hostname and name part omitting // the seperating forward slash from either. @@ -35,7 +42,7 @@ var ( // ReferenceRegexp is the full supported format of a reference. The // regexp has capturing groups for name, tag, and digest components. - ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@]([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$`) + ReferenceRegexp = regexp.MustCompile(`^((?:` + hostnameRegexp.String() + `/)?` + nameRegexp.String() + `)(?:[:](` + TagRegexp.String() + `))?(?:[@](` + DigestRegexp.String() + `))?$`) // anchoredNameRegexp is used to parse a name value, capturing hostname anchoredNameRegexp = regexp.MustCompile(`^(?:(` + hostnameRegexp.String() + `)/)?(` + nameRegexp.String() + `)$`)