aboutsummaryrefslogtreecommitdiff
path: root/libpod/image
diff options
context:
space:
mode:
Diffstat (limited to 'libpod/image')
-rw-r--r--libpod/image/filters.go83
-rw-r--r--libpod/image/image.go79
-rw-r--r--libpod/image/image_test.go55
-rw-r--r--libpod/image/parts.go123
-rw-r--r--libpod/image/parts_test.go118
-rw-r--r--libpod/image/pull.go59
-rw-r--r--libpod/image/pull_test.go28
-rw-r--r--libpod/image/utils.go10
8 files changed, 290 insertions, 265 deletions
diff --git a/libpod/image/filters.go b/libpod/image/filters.go
deleted file mode 100644
index d0c3adfb5..000000000
--- a/libpod/image/filters.go
+++ /dev/null
@@ -1,83 +0,0 @@
-package image
-
-import (
- "context"
- "strings"
- "time"
-
- "github.com/containers/libpod/pkg/inspect"
-)
-
-// ResultFilter is a mock function for image filtering
-type ResultFilter func(*Image) bool
-
-// Filter is a function to determine whether an image is included in
-// command output. Images to be outputted are tested using the function. A true
-// return will include the image, a false return will exclude it.
-type Filter func(*Image, *inspect.ImageData) bool
-
-// CreatedBeforeFilter allows you to filter on images created before
-// the given time.Time
-func CreatedBeforeFilter(createTime time.Time) ResultFilter {
- return func(i *Image) bool {
- return i.Created().Before(createTime)
- }
-}
-
-// CreatedAfterFilter allows you to filter on images created after
-// the given time.Time
-func CreatedAfterFilter(createTime time.Time) ResultFilter {
- return func(i *Image) bool {
- return i.Created().After(createTime)
- }
-}
-
-// DanglingFilter allows you to filter images for dangling images
-func DanglingFilter() ResultFilter {
- return func(i *Image) bool {
- return i.Dangling()
- }
-}
-
-// LabelFilter allows you to filter by images labels key and/or value
-func LabelFilter(ctx context.Context, labelfilter string) ResultFilter {
- // We need to handle both label=key and label=key=value
- return func(i *Image) bool {
- var value string
- splitFilter := strings.Split(labelfilter, "=")
- key := splitFilter[0]
- if len(splitFilter) > 1 {
- value = splitFilter[1]
- }
- labels, err := i.Labels(ctx)
- if err != nil {
- return false
- }
- if len(strings.TrimSpace(labels[key])) > 0 && len(strings.TrimSpace(value)) == 0 {
- return true
- }
- return labels[key] == value
- }
-}
-
-// OutputImageFilter allows you to filter by an a specific image name
-func OutputImageFilter(userImage *Image) ResultFilter {
- return func(i *Image) bool {
- return userImage.ID() == i.ID()
- }
-}
-
-// FilterImages filters images using a set of predefined filter funcs
-func FilterImages(images []*Image, filters []ResultFilter) []*Image {
- var filteredImages []*Image
- for _, image := range images {
- include := true
- for _, filter := range filters {
- include = include && filter(image)
- }
- if include {
- filteredImages = append(filteredImages, image)
- }
- }
- return filteredImages
-}
diff --git a/libpod/image/image.go b/libpod/image/image.go
index 3a6d0e305..ea326d820 100644
--- a/libpod/image/image.go
+++ b/libpod/image/image.go
@@ -25,7 +25,7 @@ import (
"github.com/containers/libpod/pkg/util"
"github.com/containers/storage"
"github.com/containers/storage/pkg/reexec"
- "github.com/opencontainers/go-digest"
+ digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -226,7 +226,6 @@ func (i *Image) getLocalImage() (*storage.Image, error) {
i.InputName = dest.DockerReference().String()
}
- var taggedName string
img, err := i.imageruntime.getImage(stripSha256(i.InputName))
if err == nil {
return img.image, err
@@ -240,25 +239,18 @@ func (i *Image) getLocalImage() (*storage.Image, error) {
return nil, err
}
- // the inputname isn't tagged, so we assume latest and try again
- if !decomposedImage.isTagged {
- taggedName = fmt.Sprintf("%s:latest", i.InputName)
- img, err = i.imageruntime.getImage(taggedName)
- if err == nil {
- return img.image, nil
- }
- }
-
// The image has a registry name in it and we made sure we looked for it locally
// with a tag. It cannot be local.
if decomposedImage.hasRegistry {
return nil, errors.Wrapf(ErrNoSuchImage, imageError)
-
}
-
// if the image is saved with the repository localhost, searching with localhost prepended is necessary
// We don't need to strip the sha because we have already determined it is not an ID
- img, err = i.imageruntime.getImage(fmt.Sprintf("%s/%s", DefaultLocalRegistry, i.InputName))
+ ref, err := decomposedImage.referenceWithRegistry(DefaultLocalRegistry)
+ if err != nil {
+ return nil, err
+ }
+ img, err = i.imageruntime.getImage(ref.String())
if err == nil {
return img.image, err
}
@@ -305,12 +297,24 @@ func (i *Image) Names() []string {
}
// RepoDigests returns a string array of repodigests associated with the image
-func (i *Image) RepoDigests() []string {
+func (i *Image) RepoDigests() ([]string, error) {
var repoDigests []string
+ digest := i.Digest()
+
for _, name := range i.Names() {
- repoDigests = append(repoDigests, strings.SplitN(name, ":", 2)[0]+"@"+i.Digest().String())
+ named, err := reference.ParseNormalizedNamed(name)
+ if err != nil {
+ return nil, err
+ }
+
+ canonical, err := reference.WithDigest(reference.TrimNamed(named), digest)
+ if err != nil {
+ return nil, err
+ }
+
+ repoDigests = append(repoDigests, canonical.String())
}
- return repoDigests
+ return repoDigests, nil
}
// Created returns the time the image was created
@@ -440,35 +444,42 @@ func getImageDigest(ctx context.Context, src types.ImageReference, sc *types.Sys
return "@" + digest.Hex(), nil
}
-// normalizeTag returns the canonical version of tag for use in Image.Names()
-func normalizeTag(tag string) (string, error) {
+// normalizedTag returns the canonical version of tag for use in Image.Names()
+func normalizedTag(tag string) (reference.Named, error) {
decomposedTag, err := decompose(tag)
if err != nil {
- return "", err
- }
- // If the input does not have a tag, we need to add one (latest)
- if !decomposedTag.isTagged {
- tag = fmt.Sprintf("%s:%s", tag, decomposedTag.tag)
+ return nil, err
}
// If the input doesn't specify a registry, set the registry to localhost
+ var ref reference.Named
if !decomposedTag.hasRegistry {
- tag = fmt.Sprintf("%s/%s", DefaultLocalRegistry, tag)
+ ref, err = decomposedTag.referenceWithRegistry(DefaultLocalRegistry)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ ref, err = decomposedTag.normalizedReference()
+ if err != nil {
+ return nil, err
+ }
}
- return tag, nil
+ // If the input does not have a tag, we need to add one (latest)
+ ref = reference.TagNameOnly(ref)
+ return ref, nil
}
// TagImage adds a tag to the given image
func (i *Image) TagImage(tag string) error {
i.reloadImage()
- tag, err := normalizeTag(tag)
+ ref, err := normalizedTag(tag)
if err != nil {
return err
}
tags := i.Names()
- if util.StringInSlice(tag, tags) {
+ if util.StringInSlice(ref.String(), tags) {
return nil
}
- tags = append(tags, tag)
+ tags = append(tags, ref.String())
if err := i.imageruntime.store.SetNames(i.ID(), tags); err != nil {
return err
}
@@ -919,21 +930,23 @@ func (i *Image) MatchRepoTag(input string) (string, error) {
if err != nil {
return "", err
}
+ imageRegistry, imageName, imageSuspiciousTagValueForSearch := dcImage.suspiciousRefNameTagValuesForSearch()
for _, repoName := range i.Names() {
count := 0
dcRepoName, err := decompose(repoName)
if err != nil {
return "", err
}
- if dcRepoName.registry == dcImage.registry && dcImage.registry != "" {
+ repoNameRegistry, repoNameName, repoNameSuspiciousTagValueForSearch := dcRepoName.suspiciousRefNameTagValuesForSearch()
+ if repoNameRegistry == imageRegistry && imageRegistry != "" {
count++
}
- if dcRepoName.name == dcImage.name && dcImage.name != "" {
+ if repoNameName == imageName && imageName != "" {
count++
- } else if splitString(dcRepoName.name) == splitString(dcImage.name) {
+ } else if splitString(repoNameName) == splitString(imageName) {
count++
}
- if dcRepoName.tag == dcImage.tag {
+ if repoNameSuspiciousTagValueForSearch == imageSuspiciousTagValueForSearch {
count++
}
results[count] = append(results[count], repoName)
diff --git a/libpod/image/image_test.go b/libpod/image/image_test.go
index 91bb2411b..077ae460e 100644
--- a/libpod/image/image_test.go
+++ b/libpod/image/image_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"github.com/containers/storage"
+ "github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert"
)
@@ -192,6 +193,51 @@ func TestImage_MatchRepoTag(t *testing.T) {
cleanup(workdir, ir)
}
+// TestImage_RepoDigests tests RepoDigest generation.
+func TestImage_RepoDigests(t *testing.T) {
+ dgst, err := digest.Parse("sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, test := range []struct {
+ name string
+ names []string
+ expected []string
+ }{
+ {
+ name: "empty",
+ names: []string{},
+ expected: nil,
+ },
+ {
+ name: "tagged",
+ names: []string{"docker.io/library/busybox:latest"},
+ expected: []string{"docker.io/library/busybox@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"},
+ },
+ {
+ name: "digest",
+ names: []string{"docker.io/library/busybox@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"},
+ expected: []string{"docker.io/library/busybox@sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc"},
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ image := &Image{
+ image: &storage.Image{
+ Names: test.names,
+ Digest: dgst,
+ },
+ }
+ actual, err := image.RepoDigests()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assert.Equal(t, test.expected, actual)
+ })
+ }
+}
+
// Test_splitString tests the splitString function in image that
// takes input and splits on / and returns the last array item
func Test_splitString(t *testing.T) {
@@ -211,24 +257,25 @@ func Test_stripSha256(t *testing.T) {
assert.Equal(t, stripSha256("sha256:a"), "a")
}
-func TestNormalizeTag(t *testing.T) {
+func TestNormalizedTag(t *testing.T) {
const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
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 + ":none"}, // Qualified name@digest; FIXME: The result is not even syntactically valid!
+ {"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
{"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 := normalizeTag(c.input)
+ res, err := normalizedTag(c.input)
if c.expected == "" {
assert.Error(t, err, c.input)
} else {
assert.NoError(t, err, c.input)
- assert.Equal(t, c.expected, res)
+ assert.Equal(t, c.expected, res.String())
}
}
}
diff --git a/libpod/image/parts.go b/libpod/image/parts.go
index 1509005e5..dfdf0b08a 100644
--- a/libpod/image/parts.go
+++ b/libpod/image/parts.go
@@ -1,79 +1,104 @@
package image
import (
- "fmt"
"strings"
"github.com/containers/image/docker/reference"
+ "github.com/pkg/errors"
)
// imageParts describes the parts of an image's name
type imageParts struct {
- transport string
- registry string
- name string
- tag string
- isTagged bool
- hasRegistry bool
+ unnormalizedRef reference.Named // WARNING: Did not go through docker.io[/library] normalization
+ hasRegistry bool
}
-// Registries must contain a ":" or a "." or be localhost
+// Registries must contain a ":" or a "." or be localhost; this helper exists for users of reference.Parse.
+// For inputs that should use the docker.io[/library] normalization, use reference.ParseNormalizedNamed instead.
func isRegistry(name string) bool {
return strings.ContainsAny(name, ".:") || name == "localhost"
}
+// GetImageBaseName uses decompose and string splits to obtain the base
+// name of an image. Doing this here because it beats changing the
+// imageParts struct names to be exported as well.
+func GetImageBaseName(input string) (string, error) {
+ decomposedImage, err := decompose(input)
+ if err != nil {
+ return "", err
+ }
+ splitImageName := strings.Split(decomposedImage.unnormalizedRef.Name(), "/")
+ return splitImageName[len(splitImageName)-1], nil
+}
+
// decompose breaks an input name into an imageParts description
func decompose(input string) (imageParts, error) {
- var (
- parts imageParts
- hasRegistry bool
- tag string
- )
imgRef, err := reference.Parse(input)
if err != nil {
- return parts, err
- }
- ntag, isTagged := imgRef.(reference.NamedTagged)
- if !isTagged {
- tag = "latest"
- if _, hasDigest := imgRef.(reference.Digested); hasDigest {
- tag = "none"
- }
- } else {
- tag = ntag.Tag()
- }
- registry := reference.Domain(imgRef.(reference.Named))
- imageName := reference.Path(imgRef.(reference.Named))
- // Is this a registry or a repo?
- if isRegistry(registry) {
- hasRegistry = true
- } else {
- if registry != "" {
- imageName = registry + "/" + imageName
- registry = ""
- }
+ return imageParts{}, err
}
+ unnormalizedNamed := imgRef.(reference.Named)
+ // ip.unnormalizedRef, because it uses reference.Parse and not reference.ParseNormalizedNamed,
+ // does not use the standard heuristics for domains vs. namespaces/repos, so we need to check
+ // explicitly.
+ hasRegistry := isRegistry(reference.Domain(unnormalizedNamed))
return imageParts{
- registry: registry,
- hasRegistry: hasRegistry,
- name: imageName,
- tag: tag,
- isTagged: isTagged,
- transport: DefaultTransport,
+ unnormalizedRef: unnormalizedNamed,
+ hasRegistry: hasRegistry,
}, nil
}
-// assemble concatenates an image's parts into a string
-func (ip *imageParts) assemble() string {
- spec := fmt.Sprintf("%s:%s", ip.name, ip.tag)
+// suspiciousRefNameTagValuesForSearch returns a "tag" value used in a previous implementation.
+// This exists only to preserve existing behavior in heuristic code; it’s dubious that that behavior is correct,
+// gespecially for the tag value.
+func (ip *imageParts) suspiciousRefNameTagValuesForSearch() (string, string, string) {
+ registry := reference.Domain(ip.unnormalizedRef)
+ imageName := reference.Path(ip.unnormalizedRef)
+ // ip.unnormalizedRef, because it uses reference.Parse and not reference.ParseNormalizedNamed,
+ // does not use the standard heuristics for domains vs. namespaces/repos.
+ if registry != "" && !isRegistry(registry) {
+ imageName = registry + "/" + imageName
+ registry = ""
+ }
- if ip.registry != "" {
- spec = fmt.Sprintf("%s/%s", ip.registry, spec)
+ var tag string
+ if tagged, isTagged := ip.unnormalizedRef.(reference.NamedTagged); isTagged {
+ tag = tagged.Tag()
+ } else if _, hasDigest := ip.unnormalizedRef.(reference.Digested); hasDigest {
+ tag = "none"
+ } else {
+ tag = "latest"
}
- return spec
+ return registry, imageName, tag
}
-// assemble concatenates an image's parts with transport into a string
-func (ip *imageParts) assembleWithTransport() string {
- return fmt.Sprintf("%s%s", ip.transport, ip.assemble())
+// referenceWithRegistry returns a (normalized) reference.Named composed of ip (with !ip.hasRegistry)
+// qualified with registry.
+func (ip *imageParts) referenceWithRegistry(registry string) (reference.Named, error) {
+ if ip.hasRegistry {
+ return nil, errors.Errorf("internal error: referenceWithRegistry called on imageParts with a registry (%#v)", *ip)
+ }
+ // We could build a reference.WithName+WithTag/WithDigest here, but we need to round-trip via a string
+ // and a ParseNormalizedNamed anyway to get the right normalization of docker.io/library, so
+ // just use a string directly.
+ qualified := registry + "/" + ip.unnormalizedRef.String()
+ ref, err := reference.ParseNormalizedNamed(qualified)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error normalizing registry+unqualified reference %#v", qualified)
+ }
+ return ref, nil
+}
+
+// normalizedReference returns a (normalized) reference for ip (with ip.hasRegistry)
+func (ip *imageParts) normalizedReference() (reference.Named, error) {
+ if !ip.hasRegistry {
+ return nil, errors.Errorf("internal error: normalizedReference called on imageParts without a registry (%#v)", *ip)
+ }
+ // We need to round-trip via a string to get the right normalization of docker.io/library
+ s := ip.unnormalizedRef.String()
+ ref, err := reference.ParseNormalizedNamed(s)
+ if err != nil { // Should never happen
+ return nil, errors.Wrapf(err, "error normalizing qualified reference %#v", s)
+ }
+ return ref, nil
}
diff --git a/libpod/image/parts_test.go b/libpod/image/parts_test.go
index 518538f0b..726e55e86 100644
--- a/libpod/image/parts_test.go
+++ b/libpod/image/parts_test.go
@@ -4,64 +4,120 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestDecompose(t *testing.T) {
const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
for _, c := range []struct {
- input string
- transport, registry, name, tag string
- isTagged, hasRegistry bool
- assembled string
- assembledWithTransport string
+ input string
+ registry, name, suspiciousTagValueForSearch string
+ hasRegistry bool
}{
- {"#", "", "", "", "", false, false, "", ""}, // Entirely invalid input
+ {"#", "", "", "", false}, // Entirely invalid input
{ // Fully qualified docker.io, name-only input
- "docker.io/library/busybox", "docker://", "docker.io", "library/busybox", "latest", false, true,
- "docker.io/library/busybox:latest", "docker://docker.io/library/busybox:latest",
+ "docker.io/library/busybox", "docker.io", "library/busybox", "latest", true,
},
{ // Fully qualified example.com, name-only input
- "example.com/ns/busybox", "docker://", "example.com", "ns/busybox", "latest", false, true,
- "example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest",
+ "example.com/ns/busybox", "example.com", "ns/busybox", "latest", true,
},
{ // Unqualified single-name input
- "busybox", "docker://", "", "busybox", "latest", false, false,
- "busybox:latest", "docker://busybox:latest",
+ "busybox", "", "busybox", "latest", false,
},
{ // Unqualified namespaced input
- "ns/busybox", "docker://", "", "ns/busybox", "latest", false, false,
- "ns/busybox:latest", "docker://ns/busybox:latest",
+ "ns/busybox", "", "ns/busybox", "latest", false,
},
{ // name:tag
- "example.com/ns/busybox:notlatest", "docker://", "example.com", "ns/busybox", "notlatest", true, true,
- "example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest",
+ "example.com/ns/busybox:notlatest", "example.com", "ns/busybox", "notlatest", true,
},
{ // name@digest
- // FIXME? .tag == "none"
- "example.com/ns/busybox" + digestSuffix, "docker://", "example.com", "ns/busybox", "none", false, true,
- // FIXME: this drops the digest and replaces it with an incorrect tag.
- "example.com/ns/busybox:none", "docker://example.com/ns/busybox:none",
+ // FIXME? .suspiciousTagValueForSearch == "none"
+ "example.com/ns/busybox" + digestSuffix, "example.com", "ns/busybox", "none", true,
},
{ // name:tag@digest
- "example.com/ns/busybox:notlatest" + digestSuffix, "docker://", "example.com", "ns/busybox", "notlatest", true, true,
- // FIXME: This drops the digest
- "example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest",
+ "example.com/ns/busybox:notlatest" + digestSuffix, "example.com", "ns/busybox", "notlatest", true,
},
} {
parts, err := decompose(c.input)
- if c.transport == "" {
+ if c.name == "" {
assert.Error(t, err, c.input)
} else {
assert.NoError(t, err, c.input)
- assert.Equal(t, c.transport, parts.transport, c.input)
- assert.Equal(t, c.registry, parts.registry, c.input)
- assert.Equal(t, c.name, parts.name, c.input)
- assert.Equal(t, c.tag, parts.tag, c.input)
- assert.Equal(t, c.isTagged, parts.isTagged, c.input)
+ registry, name, suspiciousTagValueForSearch := parts.suspiciousRefNameTagValuesForSearch()
+ assert.Equal(t, c.registry, registry, c.input)
+ assert.Equal(t, c.name, name, c.input)
+ assert.Equal(t, c.suspiciousTagValueForSearch, suspiciousTagValueForSearch, c.input)
assert.Equal(t, c.hasRegistry, parts.hasRegistry, c.input)
- assert.Equal(t, c.assembled, parts.assemble(), c.input)
- assert.Equal(t, c.assembledWithTransport, parts.assembleWithTransport(), c.input)
+ }
+ }
+}
+
+func TestImagePartsReferenceWithRegistry(t *testing.T) {
+ const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+
+ for _, c := range []struct {
+ input string
+ withDocker, withNonDocker string
+ }{
+ {"example.com/ns/busybox", "", ""}, // Fully-qualified input is invalid.
+ {"busybox", "docker.io/library/busybox", "example.com/busybox"}, // Single-name input
+ {"ns/busybox", "docker.io/ns/busybox", "example.com/ns/busybox"}, // Namespaced input
+ {"ns/busybox:notlatest", "docker.io/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}, // name:tag
+ {"ns/busybox" + digestSuffix, "docker.io/ns/busybox" + digestSuffix, "example.com/ns/busybox" + digestSuffix}, // name@digest
+ { // name:tag@digest
+ "ns/busybox:notlatest" + digestSuffix,
+ "docker.io/ns/busybox:notlatest" + digestSuffix, "example.com/ns/busybox:notlatest" + digestSuffix,
+ },
+ } {
+ parts, err := decompose(c.input)
+ require.NoError(t, err)
+ if c.withDocker == "" {
+ _, err := parts.referenceWithRegistry("docker.io")
+ assert.Error(t, err, c.input)
+ _, err = parts.referenceWithRegistry("example.com")
+ assert.Error(t, err, c.input)
+ } else {
+ ref, err := parts.referenceWithRegistry("docker.io")
+ require.NoError(t, err, c.input)
+ assert.Equal(t, c.withDocker, ref.String())
+ ref, err = parts.referenceWithRegistry("example.com")
+ require.NoError(t, err, c.input)
+ assert.Equal(t, c.withNonDocker, ref.String())
+ }
+ }
+
+ // Invalid registry value
+ parts, err := decompose("busybox")
+ require.NoError(t, err)
+ _, err = parts.referenceWithRegistry("invalid@domain")
+ assert.Error(t, err)
+}
+
+func TestImagePartsNormalizedReference(t *testing.T) {
+ const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+
+ for _, c := range []struct{ input, expected string }{
+ {"busybox", ""}, // Unqualified input is invalid
+ {"docker.io/busybox", "docker.io/library/busybox"}, // docker.io single-name
+ {"example.com/busybox", "example.com/busybox"}, // example.com single-name
+ {"docker.io/ns/busybox", "docker.io/ns/busybox"}, // docker.io namespaced
+ {"example.com/ns/busybox", "example.com/ns/busybox"}, // example.com namespaced
+ {"example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}, // name:tag
+ {"example.com/ns/busybox" + digestSuffix, "example.com/ns/busybox" + digestSuffix}, // name@digest
+ { // name:tag@digest
+ "example.com/ns/busybox:notlatest" + digestSuffix, "example.com/ns/busybox:notlatest" + digestSuffix,
+ },
+ } {
+ parts, err := decompose(c.input)
+ require.NoError(t, err)
+ if c.expected == "" {
+ _, err := parts.normalizedReference()
+ assert.Error(t, err, c.input)
+ } else {
+ ref, err := parts.normalizedReference()
+ require.NoError(t, err, c.input)
+ assert.Equal(t, c.expected, ref.String())
}
}
}
diff --git a/libpod/image/pull.go b/libpod/image/pull.go
index 09935fe7c..434b83520 100644
--- a/libpod/image/pull.go
+++ b/libpod/image/pull.go
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
- "strings"
cp "github.com/containers/image/copy"
"github.com/containers/image/directory"
@@ -76,9 +75,11 @@ func (ir *Runtime) getPullRefPair(srcRef types.ImageReference, destName string)
decomposedDest, err := decompose(destName)
if err == nil && !decomposedDest.hasRegistry {
// If the image doesn't have a registry, set it as the default repo
- decomposedDest.registry = DefaultLocalRegistry
- decomposedDest.hasRegistry = true
- destName = decomposedDest.assemble()
+ ref, err := decomposedDest.referenceWithRegistry(DefaultLocalRegistry)
+ if err != nil {
+ return pullRefPair{}, err
+ }
+ destName = ref.String()
}
reference := destName
@@ -270,12 +271,6 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
return images, nil
}
-// hasShaInInputName returns a bool as to whether the user provided an image name that includes
-// a reference to a specific sha
-func hasShaInInputName(inputName string) bool {
- return strings.Contains(inputName, "@sha256:")
-}
-
// pullGoalFromPossiblyUnqualifiedName looks at inputName and determines the possible
// image references to try pulling in combination with the registries.conf file as well
func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(inputName string) (*pullGoal, error) {
@@ -284,31 +279,11 @@ func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(inputName string) (*pullG
return nil, err
}
if decomposedImage.hasRegistry {
- var imageName, destName string
- if hasShaInInputName(inputName) {
- imageName = fmt.Sprintf("%s%s", decomposedImage.transport, inputName)
- } else {
- imageName = decomposedImage.assembleWithTransport()
- }
- srcRef, err := alltransports.ParseImageName(imageName)
+ srcRef, err := docker.ParseReference("//" + inputName)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse '%s'", inputName)
}
- if hasShaInInputName(inputName) {
- destName = decomposedImage.assemble()
- } else {
- destName = inputName
- }
- destRef, err := is.Transport.ParseStoreReference(ir.store, destName)
- if err != nil {
- return nil, errors.Wrapf(err, "error parsing dest reference name %#v", destName)
- }
- ps := pullRefPair{
- image: inputName,
- srcRef: srcRef,
- dstRef: destRef,
- }
- return singlePullRefPairGoal(ps), nil
+ return ir.getSinglePullRefPairGoal(srcRef, inputName)
}
searchRegistries, err := registries.GetRegistries()
@@ -317,22 +292,18 @@ func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(inputName string) (*pullG
}
var refPairs []pullRefPair
for _, registry := range searchRegistries {
- decomposedImage.registry = registry
- imageName := decomposedImage.assembleWithTransport()
- if hasShaInInputName(inputName) {
- imageName = fmt.Sprintf("%s%s/%s", decomposedImage.transport, registry, inputName)
- }
- srcRef, err := alltransports.ParseImageName(imageName)
+ ref, err := decomposedImage.referenceWithRegistry(registry)
if err != nil {
- return nil, errors.Wrapf(err, "unable to parse '%s'", inputName)
+ return nil, err
}
- ps := pullRefPair{
- image: decomposedImage.assemble(),
- srcRef: srcRef,
+ imageName := ref.String()
+ srcRef, err := docker.ParseReference("//" + imageName)
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to parse '%s'", imageName)
}
- ps.dstRef, err = is.Transport.ParseStoreReference(ir.store, ps.image)
+ ps, err := ir.getPullRefPair(srcRef, imageName)
if err != nil {
- return nil, errors.Wrapf(err, "error parsing dest reference name %#v", ps.image)
+ return nil, err
}
refPairs = append(refPairs, ps)
}
diff --git a/libpod/image/pull_test.go b/libpod/image/pull_test.go
index 37b45dc83..3890c5e6c 100644
--- a/libpod/image/pull_test.go
+++ b/libpod/image/pull_test.go
@@ -76,9 +76,7 @@ func TestGetPullRefPair(t *testing.T) {
},
{ // name, no registry, no tag:
"dir:/dev/this-does-not-exist", "from-directory",
- // FIXME(?) Adding a registry also adds a :latest tag. OTOH that actually matches the used destination.
- // Either way it is surprising that the localhost/ addition changes this. (mitr hoping to remove the "image" member).
- "localhost/from-directory:latest", "localhost/from-directory:latest",
+ "localhost/from-directory", "localhost/from-directory:latest",
},
{ // registry/name:tag :
"dir:/dev/this-does-not-exist", "example.com/from-directory:notlatest",
@@ -90,8 +88,7 @@ func TestGetPullRefPair(t *testing.T) {
},
{ // name@digest, no registry:
"dir:/dev/this-does-not-exist", "from-directory" + digestSuffix,
- // FIXME?! Why is this dropping the digest, and adding :none?!
- "localhost/from-directory:none", "localhost/from-directory:none",
+ "localhost/from-directory" + digestSuffix, "localhost/from-directory" + digestSuffix,
},
{ // registry/name@digest:
"dir:/dev/this-does-not-exist", "example.com/from-directory" + digestSuffix,
@@ -211,14 +208,13 @@ func TestPullGoalFromImageReference(t *testing.T) {
false,
},
{ // Relative path, single element.
- // FIXME? Note the :latest difference in .image.
"dir:this-does-not-exist",
- []expected{{"localhost/this-does-not-exist:latest", "localhost/this-does-not-exist:latest"}},
+ []expected{{"localhost/this-does-not-exist", "localhost/this-does-not-exist:latest"}},
false,
},
{ // Relative path, multiple elements.
"dir:testdata/this-does-not-exist",
- []expected{{"localhost/testdata/this-does-not-exist:latest", "localhost/testdata/this-does-not-exist:latest"}},
+ []expected{{"localhost/testdata/this-does-not-exist", "localhost/testdata/this-does-not-exist:latest"}},
false,
},
@@ -324,8 +320,7 @@ func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
{ // Qualified example.com, name@digest.
"example.com/ns/busybox" + digestSuffix,
[]pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix,
- // FIXME?! Why is .dstName dropping the digest, and adding :none?!
- "example.com/ns/busybox:none"}},
+ "example.com/ns/busybox" + digestSuffix}},
false,
},
// Qualified example.com, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input.
@@ -333,16 +328,16 @@ func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
{ // Unqualified, single-name, name-only
"busybox",
[]pullRefStrings{
- {"example.com/busybox:latest", "docker://example.com/busybox:latest", "example.com/busybox:latest"},
+ {"example.com/busybox", "docker://example.com/busybox:latest", "example.com/busybox:latest"},
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
- {"docker.io/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"},
+ {"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"},
},
true,
},
{ // Unqualified, namespaced, name-only
"ns/busybox",
[]pullRefStrings{
- {"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"},
+ {"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"},
},
true,
},
@@ -351,17 +346,16 @@ func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
[]pullRefStrings{
{"example.com/busybox:notlatest", "docker://example.com/busybox:notlatest", "example.com/busybox:notlatest"},
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
- {"docker.io/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"},
+ {"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"},
},
true,
},
{ // Unqualified, name@digest
"busybox" + digestSuffix,
[]pullRefStrings{
- // FIXME?! Why is .input and .dstName dropping the digest, and adding :none?!
- {"example.com/busybox:none", "docker://example.com/busybox" + digestSuffix, "example.com/busybox:none"},
+ {"example.com/busybox" + digestSuffix, "docker://example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix},
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
- {"docker.io/busybox:none", "docker://busybox" + digestSuffix, "docker.io/library/busybox:none"},
+ {"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix},
},
true,
},
diff --git a/libpod/image/utils.go b/libpod/image/utils.go
index b944de1bb..ad027f32a 100644
--- a/libpod/image/utils.go
+++ b/libpod/image/utils.go
@@ -17,6 +17,7 @@ import (
// findImageInRepotags takes an imageParts struct and searches images' repotags for
// a match on name:tag
func findImageInRepotags(search imageParts, images []*Image) (*storage.Image, error) {
+ _, searchName, searchSuspiciousTagValueForSearch := search.suspiciousRefNameTagValuesForSearch()
var results []*storage.Image
for _, image := range images {
for _, name := range image.Names() {
@@ -25,21 +26,22 @@ func findImageInRepotags(search imageParts, images []*Image) (*storage.Image, er
if err != nil {
continue
}
- if d.name == search.name && d.tag == search.tag {
+ _, dName, dSuspiciousTagValueForSearch := d.suspiciousRefNameTagValuesForSearch()
+ if dName == searchName && dSuspiciousTagValueForSearch == searchSuspiciousTagValueForSearch {
results = append(results, image.image)
continue
}
// account for registry:/somedir/image
- if strings.HasSuffix(d.name, search.name) && d.tag == search.tag {
+ if strings.HasSuffix(dName, searchName) && dSuspiciousTagValueForSearch == searchSuspiciousTagValueForSearch {
results = append(results, image.image)
continue
}
}
}
if len(results) == 0 {
- return &storage.Image{}, errors.Errorf("unable to find a name and tag match for %s in repotags", search.name)
+ return &storage.Image{}, errors.Errorf("unable to find a name and tag match for %s in repotags", searchName)
} else if len(results) > 1 {
- return &storage.Image{}, errors.Errorf("found multiple name and tag matches for %s in repotags", search.name)
+ return &storage.Image{}, errors.Errorf("found multiple name and tag matches for %s in repotags", searchName)
}
return results[0], nil
}