Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support tag@digest notation #579

Merged
merged 1 commit into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions libimage/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/containers/image/v5/docker/reference"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

// NormalizeName normalizes the provided name according to the conventions by
Expand Down Expand Up @@ -40,6 +41,11 @@ func NormalizeName(name string) (reference.Named, error) {
}

if _, hasTag := named.(reference.NamedTagged); hasTag {
vrothberg marked this conversation as resolved.
Show resolved Hide resolved
// Strip off the tag of a tagged and digested reference.
named, err = normalizeTaggedDigestedNamed(named)
if err != nil {
return nil, err
}
return named, nil
}
if _, hasDigest := named.(reference.Digested); hasDigest {
Expand Down Expand Up @@ -90,3 +96,48 @@ func ToNameTagPairs(repoTags []reference.Named) ([]NameTagPair, error) {
}
return pairs, nil
}

// normalizeTaggedDigestedString strips the tag off the specified string iff it
// is tagged and digested. Note that the tag is entirely ignored to match
// Docker behavior.
func normalizeTaggedDigestedString(s string) (string, error) {
// Note that the input string is not expected to be parseable, so we
// return it verbatim in error cases.
vrothberg marked this conversation as resolved.
Show resolved Hide resolved
ref, err := reference.Parse(s)
if err != nil {
return "", err
}
named, ok := ref.(reference.Named)
if !ok {
return s, nil
}
named, err = normalizeTaggedDigestedNamed(named)
if err != nil {
return "", err
}
return named.String(), nil
}

// normalizeTaggedDigestedNamed strips the tag off the specified named
// reference iff it is tagged and digested. Note that the tag is entirely
// ignored to match Docker behavior.
func normalizeTaggedDigestedNamed(named reference.Named) (reference.Named, error) {
_, isTagged := named.(reference.NamedTagged)
if !isTagged {
return named, nil
}
digested, isDigested := named.(reference.Digested)
if !isDigested {
return named, nil
}

// Now strip off the tag.
newNamed := reference.TrimNamed(named)
// And re-add the digest.
newNamed, err := reference.WithDigest(newNamed, digested.Digest())
if err != nil {
return named, err
}
logrus.Debugf("Stripped off tag from tagged and digested reference %q", named.String())
return newNamed, nil
}
44 changes: 36 additions & 8 deletions libimage/normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ func TestNormalizeName(t *testing.T) {

for _, c := range []struct{ input, expected string }{
{"#", ""}, // Clearly invalid
{"example.com/busybox", "example.com/busybox:latest"}, // Qualified name-only
{"example.com/busybox:notlatest", "example.com/busybox:notlatest"}, // Qualified name:tag
{"example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix}, // Qualified name@digest; FIXME? Should we allow tagging with a digest at all?
{"example.com/busybox:notlatest" + digestSuffix, "example.com/busybox:notlatest" + digestSuffix}, // Qualified name:tag@digest
{"busybox:latest", "localhost/busybox:latest"}, // Unqualified name-only
{"localhost/busybox", "localhost/busybox:latest"}, // Qualified with localhost
{"ns/busybox:latest", "localhost/ns/busybox:latest"}, // Unqualified with a dot-less namespace
{"docker.io/busybox:latest", "docker.io/library/busybox:latest"}, // docker.io without /library/
{"example.com/busybox", "example.com/busybox:latest"}, // Qualified name-only
{"example.com/busybox:notlatest", "example.com/busybox:notlatest"}, // Qualified name:tag
{"example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix}, // Qualified name@digest
{"example.com/busybox:notlatest" + digestSuffix, "example.com/busybox" + digestSuffix}, // Qualified name:tag@digest
{"busybox:latest", "localhost/busybox:latest"}, // Unqualified name-only
{"busybox:latest" + digestSuffix, "localhost/busybox" + digestSuffix}, // Unqualified name:tag@digest
{"localhost/busybox", "localhost/busybox:latest"}, // Qualified with localhost
{"ns/busybox:latest", "localhost/ns/busybox:latest"}, // Unqualified with a dot-less namespace
{"docker.io/busybox:latest", "docker.io/library/busybox:latest"}, // docker.io without /library/
} {
res, err := NormalizeName(c.input)
if c.expected == "" {
Expand All @@ -30,3 +31,30 @@ func TestNormalizeName(t *testing.T) {
}
}
}

func TestNormalizeTaggedDigestedString(t *testing.T) {
const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

for _, test := range []struct{ input, expected string }{
{"$$garbage", ""},
{"fedora", "fedora"},
{"fedora:tag", "fedora:tag"},
{digestSuffix, ""},
{"docker://fedora:latest", ""},
{"docker://fedora:latest" + digestSuffix, ""},
{"fedora" + digestSuffix, "fedora" + digestSuffix},
{"fedora:latest" + digestSuffix, "fedora" + digestSuffix},
{"repo/fedora:123456" + digestSuffix, "repo/fedora" + digestSuffix},
{"quay.io/repo/fedora:tag" + digestSuffix, "quay.io/repo/fedora" + digestSuffix},
{"localhost/fedora:anothertag" + digestSuffix, "localhost/fedora" + digestSuffix},
{"localhost:5000/fedora:v1.2.3.4.5" + digestSuffix, "localhost:5000/fedora" + digestSuffix},
} {
res, err := normalizeTaggedDigestedString(test.input)
if test.expected == "" {
assert.Error(t, err, "%v", test)
} else {
assert.NoError(t, err, "%v", test)
assert.Equal(t, test.expected, res, "%v", test)
}
}
}
9 changes: 9 additions & 0 deletions libimage/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ func (r *Runtime) Pull(ctx context.Context, name string, pullPolicy config.PullP
return []*Image{local}, err
}

// Docker compat: strip off the tag iff name is tagged and digested
// (e.g., fedora:latest@sha256...). In that case, the tag is stripped
// off and entirely ignored. The digest is the sole source of truth.
normalizedName, normalizeError := normalizeTaggedDigestedString(name)
if normalizeError != nil {
return nil, normalizeError
}
name = normalizedName

// If the input does not include a transport assume it refers
// to a registry.
dockerRef, dockerErr := alltransports.ParseImageName("docker://" + name)
Expand Down
2 changes: 2 additions & 0 deletions libimage/pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func TestPull(t *testing.T) {
{"docker://alpine", false, 1, []string{"docker.io/library/alpine:latest"}},
{"docker.io/library/alpine", false, 1, []string{"docker.io/library/alpine:latest"}},
{"docker://docker.io/library/alpine", false, 1, []string{"docker.io/library/alpine:latest"}},
{"quay.io/libpod/alpine@sha256:634a8f35b5f16dcf4aaa0822adc0b1964bb786fca12f6831de8ddc45e5986a00", false, 1, []string{"quay.io/libpod/alpine@sha256:634a8f35b5f16dcf4aaa0822adc0b1964bb786fca12f6831de8ddc45e5986a00"}},
{"quay.io/libpod/alpine:pleaseignorethistag@sha256:634a8f35b5f16dcf4aaa0822adc0b1964bb786fca12f6831de8ddc45e5986a00", false, 1, []string{"quay.io/libpod/alpine@sha256:634a8f35b5f16dcf4aaa0822adc0b1964bb786fca12f6831de8ddc45e5986a00"}},
} {
pulledImages, err := runtime.Pull(ctx, test.input, config.PullPolicyAlways, pullOptions)
if test.expectError {
Expand Down
9 changes: 9 additions & 0 deletions libimage/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ func (r *Runtime) LookupImage(name string, options *LookupImageOptions) (*Image,
}
logrus.Debugf("Found image %q in local containers storage (%s)", name, storageRef.StringWithinTransport())
return r.storageToImage(img, storageRef), "", nil
} else {
// Docker compat: strip off the tag iff name is tagged and digested
// (e.g., fedora:latest@sha256...). In that case, the tag is stripped
// off and entirely ignored. The digest is the sole source of truth.
normalizedName, err := normalizeTaggedDigestedString(name)
if err != nil {
return nil, "", err
}
name = normalizedName
}

originalName := name
Expand Down