Skip to content

Commit

Permalink
support tag@digest notation
Browse files Browse the repository at this point in the history
For the sake of Docker compatibility, support the tag@digest notation.
In that case, the tag is stripped off the reference and the digest is
sole source of truth.

Add a number of tests to make sure we're behaving as expected.

Context: containers/podman/issues/6721
Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
  • Loading branch information
vrothberg committed May 26, 2021
1 parent ea4f702 commit a022bab
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 8 deletions.
43 changes: 43 additions & 0 deletions libimage/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func NormalizeName(name string) (reference.Named, error) {
return nil, err
}

// Strip off the tag of a tagged and digested reference.
named = normalizeTaggedDigestedNamed(named)

if _, hasTag := named.(reference.NamedTagged); hasTag {
return named, nil
}
Expand Down Expand Up @@ -90,3 +93,43 @@ 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 {
// Note that the input string is not expected to be parseable, so we
// return it verbatim in error cases.
ref, err := reference.Parse(s)
if err != nil {
return s
}
named, ok := ref.(reference.Named)
if !ok {
return s
}
return normalizeTaggedDigestedNamed(named).String()
}

// 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 {
_, isTagged := named.(reference.NamedTagged)
if !isTagged {
return named
}
digested, isDigested := named.(reference.Digested)
if !isDigested {
return named
}

// 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
}
return newNamed
}
37 changes: 29 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,23 @@ func TestNormalizeName(t *testing.T) {
}
}
}

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

for _, test := range []struct{ input, expected string }{
{"$$garbage", "$$garbage"},
{"fedora", "fedora"},
{"fedora:tag", "fedora:tag"},
{"docker://fedora:latest", "docker://fedora:latest"},
{"docker://fedora:latest"+digestSuffix, "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},
} {
assert.Equal(t, test.expected, normalizeTaggedDigestedString(test.input), "%v", test)
}
}
5 changes: 5 additions & 0 deletions libimage/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ func (r *Runtime) Pull(ctx context.Context, name string, pullPolicy config.PullP
options = &PullOptions{}
}

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

ref, err := alltransports.ParseImageName(name)
if err != nil {
// If the image clearly refers to a local one, we can look it up directly.
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
5 changes: 5 additions & 0 deletions libimage/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ func (r *Runtime) LookupImage(name string, options *LookupImageOptions) (*Image,
options = &LookupImageOptions{}
}

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

// If needed extract the name sans transport.
storageRef, err := alltransports.ParseImageName(name)
if err == nil {
Expand Down

0 comments on commit a022bab

Please sign in to comment.