diff options
Diffstat (limited to 'libpod/image/pull_test.go')
-rw-r--r-- | libpod/image/pull_test.go | 320 |
1 files changed, 295 insertions, 25 deletions
diff --git a/libpod/image/pull_test.go b/libpod/image/pull_test.go index 535dd4e9d..5ef8c47a5 100644 --- a/libpod/image/pull_test.go +++ b/libpod/image/pull_test.go @@ -1,24 +1,278 @@ package image import ( + "context" + "fmt" "io/ioutil" "os" + "path/filepath" + "strings" "testing" "github.com/containers/image/transports" + "github.com/containers/image/transports/alltransports" + "github.com/containers/image/types" + "github.com/containers/storage" + "github.com/containers/storage/pkg/idtools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// newTestRuntime returns a *Runtime implementation and a cleanup function which the caller is expected to call. +func newTestRuntime(t *testing.T) (*Runtime, func()) { + wd, err := ioutil.TempDir("", "testStorageRuntime") + require.NoError(t, err) + err = os.MkdirAll(wd, 0700) + require.NoError(t, err) + + store, err := storage.GetStore(storage.StoreOptions{ + RunRoot: filepath.Join(wd, "run"), + GraphRoot: filepath.Join(wd, "root"), + GraphDriverName: "vfs", + GraphDriverOptions: []string{}, + UIDMap: []idtools.IDMap{{ + ContainerID: 0, + HostID: os.Getuid(), + Size: 1, + }}, + GIDMap: []idtools.IDMap{{ + ContainerID: 0, + HostID: os.Getgid(), + Size: 1, + }}, + }) + require.NoError(t, err) + + ir := NewImageRuntimeFromStore(store) + cleanup := func() { _ = os.RemoveAll(wd) } + return ir, cleanup +} + +// storageReferenceWithoutLocation returns ref.StringWithinTransport(), +// stripping the [store-specification] prefix from containers/image/storage reference format. +func storageReferenceWithoutLocation(ref types.ImageReference) string { + res := ref.StringWithinTransport() + if res[0] == '[' { + closeIndex := strings.IndexRune(res, ']') + if closeIndex > 0 { + res = res[closeIndex+1:] + } + } + return res +} + +func TestGetPullRefPair(t *testing.T) { + const imageID = "@0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + + ir, cleanup := newTestRuntime(t) + defer cleanup() + + for _, c := range []struct{ srcName, destName, expectedImage, expectedDstName string }{ + // == Source does not have a Docker reference (as is the case for docker-archive:, oci-archive, dir:); destination formats: + { // registry/name, no tag: + "dir:/dev/this-does-not-exist", "example.com/from-directory", + "example.com/from-directory", "example.com/from-directory:latest", + }, + { // 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", + }, + { // registry/name:tag : + "dir:/dev/this-does-not-exist", "example.com/from-directory:notlatest", + "example.com/from-directory:notlatest", "example.com/from-directory:notlatest", + }, + { // name:tag, no registry: + "dir:/dev/this-does-not-exist", "from-directory:notlatest", + "localhost/from-directory:notlatest", "localhost/from-directory:notlatest", + }, + { // 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", + }, + { // registry/name@digest: + "dir:/dev/this-does-not-exist", "example.com/from-directory" + digestSuffix, + "example.com/from-directory" + digestSuffix, "example.com/from-directory" + digestSuffix, + }, + { // ns/name:tag, no registry: + // FIXME: This is interpreted as "registry == ns" + "dir:/dev/this-does-not-exist", "ns/from-directory:notlatest", + "ns/from-directory:notlatest", "docker.io/ns/from-directory:notlatest", + }, + { // containers-storage image ID + "dir:/dev/this-does-not-exist", imageID, + imageID, imageID, + }, + // == Source does have a Docker reference. + // In that case getPullListFromRef uses the full transport:name input as a destName, + // which would be invalid in the returned dstName - but dstName is derived from the source, so it does not really matter _so_ much. + // Note that unlike real-world use we use different :source and :destination to verify the data flow in more detail. + { // registry/name:tag + "docker://example.com/busybox:source", "docker://example.com/busybox:destination", + "docker://example.com/busybox:destination", "example.com/busybox:source", + }, + { // Implied docker.io/library and :latest + "docker://busybox", "docker://busybox:destination", + "docker://busybox:destination", "docker.io/library/busybox:latest", + }, + // == Invalid destination format. + {"tarball:/dev/null", "tarball:/dev/null", "", ""}, + } { + testDescription := fmt.Sprintf("%#v %#v", c.srcName, c.destName) + srcRef, err := alltransports.ParseImageName(c.srcName) + require.NoError(t, err, testDescription) + + res, err := ir.getPullRefPair(srcRef, c.destName) + if c.expectedDstName == "" { + assert.Error(t, err, testDescription) + } else { + require.NoError(t, err, testDescription) + assert.Equal(t, c.expectedImage, res.image, testDescription) + assert.Equal(t, srcRef, res.srcRef, testDescription) + assert.Equal(t, c.expectedDstName, storageReferenceWithoutLocation(res.dstRef), testDescription) + } + } +} + +func TestPullGoalFromImageReference(t *testing.T) { + ir, cleanup := newTestRuntime(t) + defer cleanup() + + type expected struct{ image, dstName string } + for _, c := range []struct { + srcName string + expected []expected + expectedPullAllPairs bool + }{ + // == docker-archive: + {"docker-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. + {"docker-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. + // FIXME: The implementation has extra code for len(manifest) == 0?! That will fail in getImageDigest anyway. + { // RepoTags is empty + "docker-archive:testdata/docker-unnamed.tar.xz", + []expected{{"@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951", "@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951"}}, + false, + }, + { // RepoTags is a [docker.io/library/]name:latest, normalized to the short format. + "docker-archive:testdata/docker-name-only.tar.xz", + []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, + true, + }, + { // RepoTags is a registry/name:latest + "docker-archive:testdata/docker-registry-name.tar.xz", + []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, + true, + }, + { // RepoTags has multiple items for a single image + "docker-archive:testdata/docker-two-names.tar.xz", + []expected{ + {"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}, + {"example.com/empty:latest", "example.com/empty:latest"}, + }, + true, + }, + { // FIXME: Two images in a single archive - only the "first" one (whichever it is) is returned + // (and docker-archive: then refuses to read anything when the manifest has more than 1 item) + "docker-archive:testdata/docker-two-images.tar.xz", + []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, + // "example.com/empty/but:different" exists but is ignored + true, + }, + + // == oci-archive: + {"oci-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. + {"oci-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. + // FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged. + // { // No name annotation + // "oci-archive:testdata/oci-unnamed.tar.gz", + // []expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}}, + // false, + // }, + // { // Name is a name:latest (no normalization is defined). + // "oci-archive:testdata/oci-name-only.tar.gz", + // []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, + // false, + // }, + // { // Name is a registry/name:latest + // "oci-archive:testdata/oci-registry-name.tar.gz", + // []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, + // false, + // }, + // // Name exists, but is an invalid Docker reference; such names will fail when creating dstReference. + // {"oci-archive:testdata/oci-non-docker-name.tar.gz", nil, false}, + // Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef. + + // == dir: + { // Absolute path + "dir:/dev/this-does-not-exist", + []expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist:latest"}}, + 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"}}, + false, + }, + { // Relative path, multiple elements. + // FIXME: This does not add localhost/, so dstName is normalized to docker.io/testdata. + "dir:testdata/this-does-not-exist", + []expected{{"testdata/this-does-not-exist", "docker.io/testdata/this-does-not-exist:latest"}}, + false, + }, + + // == Others, notably: + // === docker:// (has ImageReference.DockerReference) + { // Fully-specified input + "docker://docker.io/library/busybox:latest", + []expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}}, + false, + }, + { // Minimal form of the input + "docker://busybox", + []expected{{"docker://busybox", "docker.io/library/busybox:latest"}}, + false, + }, + + // === tarball: (as an example of what happens when ImageReference.DockerReference is nil). + // FIXME? This tries to parse "tarball:/dev/null" as a storageReference, and fails. + // (This is NOT an API promise that the results will continue to be this way.) + {"tarball:/dev/null", nil, false}, + } { + srcRef, err := alltransports.ParseImageName(c.srcName) + require.NoError(t, err, c.srcName) + + res, err := ir.pullGoalFromImageReference(context.Background(), srcRef, c.srcName, nil) + if len(c.expected) == 0 { + assert.Error(t, err, c.srcName) + } else { + require.NoError(t, err, c.srcName) + require.Len(t, res.refPairs, len(c.expected), c.srcName) + for i, e := range c.expected { + testDescription := fmt.Sprintf("%s #%d", c.srcName, i) + assert.Equal(t, e.image, res.refPairs[i].image, testDescription) + assert.Equal(t, srcRef, res.refPairs[i].srcRef, testDescription) + assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) + } + assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName) + assert.False(t, res.usedSearchRegistries, c.srcName) + assert.Nil(t, res.searchedRegistries, c.srcName) + } + } +} + const registriesConfWithSearch = `[registries.search] registries = ['example.com', 'docker.io'] ` -func TestRefNamesFromPossiblyUnqualifiedName(t *testing.T) { +func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) { const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - type pullRefStrings struct{ image, srcRef, dstName string } // pullRefName with string data only + type pullRefStrings struct{ image, srcRef, dstName string } // pullRefPair with string data only - registriesConf, err := ioutil.TempFile("", "TestRefNamesFromPossiblyUnqualifiedName") + registriesConf, err := ioutil.TempFile("", "TestPullGoalFromPossiblyUnqualifiedName") require.NoError(t, err) defer registriesConf.Close() defer os.Remove(registriesConf.Name()) @@ -26,6 +280,9 @@ func TestRefNamesFromPossiblyUnqualifiedName(t *testing.T) { err = ioutil.WriteFile(registriesConf.Name(), []byte(registriesConfWithSearch), 0600) require.NoError(t, err) + ir, cleanup := newTestRuntime(t) + defer cleanup() + // Environment is per-process, so this looks very unsafe; actually it seems fine because tests are not // run in parallel unless they opt in by calling t.Parallel(). So don’t do that. oldRCP, hasRCP := os.LookupEnv("REGISTRIES_CONFIG_PATH") @@ -39,60 +296,68 @@ func TestRefNamesFromPossiblyUnqualifiedName(t *testing.T) { os.Setenv("REGISTRIES_CONFIG_PATH", registriesConf.Name()) for _, c := range []struct { - input string - expected []pullRefStrings + input string + expected []pullRefStrings + expectedUsedSearchRegistries bool }{ - {"#", nil}, // Clearly invalid. + {"#", nil, false}, // Clearly invalid. { // Fully-explicit docker.io, name-only. "docker.io/library/busybox", // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - []pullRefStrings{{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox"}}, + []pullRefStrings{{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, + false, }, { // docker.io with implied /library/, name-only. "docker.io/busybox", // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) - // The .dstName fields differ for the explicit/implicit /library/ cases, but StorageTransport.ParseStoreReference normalizes that. - []pullRefStrings{{"docker.io/busybox", "docker://busybox:latest", "docker.io/busybox"}}, + []pullRefStrings{{"docker.io/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, + false, }, { // Qualified example.com, name-only. "example.com/ns/busybox", - []pullRefStrings{{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox"}}, + []pullRefStrings{{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}}, + false, }, { // Qualified example.com, name:tag. "example.com/ns/busybox:notlatest", []pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}}, + false, }, { // 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"}}, + false, }, // Qualified example.com, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. - {"example.com/ns/busybox:notlatest" + digestSuffix, nil}, + {"example.com/ns/busybox:notlatest" + digestSuffix, nil, false}, { // Unqualified, single-name, name-only "busybox", []pullRefStrings{ {"example.com/busybox:latest", "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/busybox:latest"}, + {"docker.io/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}, }, + true, }, { // Unqualified, namespaced, name-only "ns/busybox", // FIXME: This is interpreted as "registry == ns", and actual pull happens from docker.io/ns/busybox:latest; // example.com should be first in the list but isn't used at all. []pullRefStrings{ - {"ns/busybox", "docker://ns/busybox:latest", "ns/busybox"}, + {"ns/busybox", "docker://ns/busybox:latest", "docker.io/ns/busybox:latest"}, }, + false, }, { // Unqualified, name:tag "busybox:notlatest", []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/busybox:notlatest"}, + {"docker.io/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"}, }, + true, }, { // Unqualified, name@digest "busybox" + digestSuffix, @@ -100,26 +365,31 @@ func TestRefNamesFromPossiblyUnqualifiedName(t *testing.T) { // 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"}, // (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/busybox:none"}, + {"docker.io/busybox:none", "docker://busybox" + digestSuffix, "docker.io/library/busybox:none"}, }, + true, }, // Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. - {"busybox:notlatest" + digestSuffix, nil}, + {"busybox:notlatest" + digestSuffix, nil, false}, } { - res, err := refNamesFromPossiblyUnqualifiedName(c.input) + res, err := ir.pullGoalFromPossiblyUnqualifiedName(c.input) if len(c.expected) == 0 { assert.Error(t, err, c.input) } else { assert.NoError(t, err, c.input) - strings := make([]pullRefStrings, len(res)) - for i, rn := range res { - strings[i] = pullRefStrings{ - image: rn.image, - srcRef: transports.ImageName(rn.srcRef), - dstName: rn.dstName, - } + for i, e := range c.expected { + testDescription := fmt.Sprintf("%s #%d", c.input, i) + assert.Equal(t, e.image, res.refPairs[i].image, testDescription) + assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription) + assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) + } + assert.False(t, res.pullAllPairs, c.input) + assert.Equal(t, c.expectedUsedSearchRegistries, res.usedSearchRegistries, c.input) + if !c.expectedUsedSearchRegistries { + assert.Nil(t, res.searchedRegistries, c.input) + } else { + assert.Equal(t, []string{"example.com", "docker.io"}, res.searchedRegistries, c.input) } - assert.Equal(t, c.expected, strings, c.input) } } } |