github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/libpod/image/pull_test.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/containers/image/v5/transports"
    13  	"github.com/containers/image/v5/transports/alltransports"
    14  	"github.com/containers/image/v5/types"
    15  	"github.com/containers/storage"
    16  	"github.com/containers/storage/pkg/idtools"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  // newTestRuntime returns a *Runtime implementation and a cleanup function which the caller is expected to call.
    22  func newTestRuntime(t *testing.T) (*Runtime, func()) {
    23  	wd, err := ioutil.TempDir("", "testStorageRuntime")
    24  	require.NoError(t, err)
    25  	err = os.MkdirAll(wd, 0700)
    26  	require.NoError(t, err)
    27  
    28  	store, err := storage.GetStore(storage.StoreOptions{
    29  		RunRoot:            filepath.Join(wd, "run"),
    30  		GraphRoot:          filepath.Join(wd, "root"),
    31  		GraphDriverName:    "vfs",
    32  		GraphDriverOptions: []string{},
    33  		UIDMap: []idtools.IDMap{{
    34  			ContainerID: 0,
    35  			HostID:      os.Getuid(),
    36  			Size:        1,
    37  		}},
    38  		GIDMap: []idtools.IDMap{{
    39  			ContainerID: 0,
    40  			HostID:      os.Getgid(),
    41  			Size:        1,
    42  		}},
    43  	})
    44  	require.NoError(t, err)
    45  
    46  	ir := NewImageRuntimeFromStore(store)
    47  	cleanup := func() { _ = os.RemoveAll(wd) }
    48  	return ir, cleanup
    49  }
    50  
    51  // storageReferenceWithoutLocation returns ref.StringWithinTransport(),
    52  // stripping the [store-specification] prefix from containers/image/storage reference format.
    53  func storageReferenceWithoutLocation(ref types.ImageReference) string {
    54  	res := ref.StringWithinTransport()
    55  	if res[0] == '[' {
    56  		closeIndex := strings.IndexRune(res, ']')
    57  		if closeIndex > 0 {
    58  			res = res[closeIndex+1:]
    59  		}
    60  	}
    61  	return res
    62  }
    63  
    64  func TestGetPullRefPair(t *testing.T) {
    65  	const imageID = "@0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    66  	const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    67  
    68  	ir, cleanup := newTestRuntime(t)
    69  	defer cleanup()
    70  
    71  	for _, c := range []struct{ srcName, destName, expectedImage, expectedDstName string }{
    72  		// == Source does not have a Docker reference (as is the case for docker-archive:, oci-archive, dir:); destination formats:
    73  		{ // registry/name, no tag:
    74  			"dir:/dev/this-does-not-exist", "example.com/from-directory",
    75  			"example.com/from-directory", "example.com/from-directory:latest",
    76  		},
    77  		{ // name, no registry, no tag:
    78  			"dir:/dev/this-does-not-exist", "from-directory",
    79  			"localhost/from-directory", "localhost/from-directory:latest",
    80  		},
    81  		{ // registry/name:tag :
    82  			"dir:/dev/this-does-not-exist", "example.com/from-directory:notlatest",
    83  			"example.com/from-directory:notlatest", "example.com/from-directory:notlatest",
    84  		},
    85  		{ // name:tag, no registry:
    86  			"dir:/dev/this-does-not-exist", "from-directory:notlatest",
    87  			"localhost/from-directory:notlatest", "localhost/from-directory:notlatest",
    88  		},
    89  		{ // name@digest, no registry:
    90  			"dir:/dev/this-does-not-exist", "from-directory" + digestSuffix,
    91  			"localhost/from-directory" + digestSuffix, "localhost/from-directory" + digestSuffix,
    92  		},
    93  		{ // registry/name@digest:
    94  			"dir:/dev/this-does-not-exist", "example.com/from-directory" + digestSuffix,
    95  			"example.com/from-directory" + digestSuffix, "example.com/from-directory" + digestSuffix,
    96  		},
    97  		{ // ns/name:tag, no registry:
    98  			"dir:/dev/this-does-not-exist", "ns/from-directory:notlatest",
    99  			"localhost/ns/from-directory:notlatest", "localhost/ns/from-directory:notlatest",
   100  		},
   101  		{ // containers-storage image ID
   102  			"dir:/dev/this-does-not-exist", imageID,
   103  			imageID, imageID,
   104  		},
   105  		// == Source does have a Docker reference.
   106  		// In that case getPullListFromRef uses the full transport:name input as a destName,
   107  		// which would be invalid in the returned dstName - but dstName is derived from the source, so it does not really matter _so_ much.
   108  		// Note that unlike real-world use we use different :source and :destination to verify the data flow in more detail.
   109  		{ // registry/name:tag
   110  			"docker://example.com/busybox:source", "docker://example.com/busybox:destination",
   111  			"docker://example.com/busybox:destination", "example.com/busybox:source",
   112  		},
   113  		{ // Implied docker.io/library and :latest
   114  			"docker://busybox", "docker://busybox:destination",
   115  			"docker://busybox:destination", "docker.io/library/busybox:latest",
   116  		},
   117  		// == Invalid destination format.
   118  		{"tarball:/dev/null", "tarball:/dev/null", "", ""},
   119  	} {
   120  		testDescription := fmt.Sprintf("%#v %#v", c.srcName, c.destName)
   121  		srcRef, err := alltransports.ParseImageName(c.srcName)
   122  		require.NoError(t, err, testDescription)
   123  
   124  		res, err := ir.getPullRefPair(srcRef, c.destName)
   125  		if c.expectedDstName == "" {
   126  			assert.Error(t, err, testDescription)
   127  		} else {
   128  			require.NoError(t, err, testDescription)
   129  			assert.Equal(t, c.expectedImage, res.image, testDescription)
   130  			assert.Equal(t, srcRef, res.srcRef, testDescription)
   131  			assert.Equal(t, c.expectedDstName, storageReferenceWithoutLocation(res.dstRef), testDescription)
   132  		}
   133  	}
   134  }
   135  
   136  func TestPullGoalFromImageReference(t *testing.T) {
   137  	ir, cleanup := newTestRuntime(t)
   138  	defer cleanup()
   139  
   140  	type expected struct{ image, dstName string }
   141  	for _, c := range []struct {
   142  		srcName              string
   143  		expected             []expected
   144  		expectedPullAllPairs bool
   145  	}{
   146  		// == docker-archive:
   147  		{"docker-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist.
   148  		{"docker-archive:/dev/null", nil, false},                // Input exists but does not contain a manifest.
   149  		// FIXME: The implementation has extra code for len(manifest) == 0?! That will fail in getImageDigest anyway.
   150  		{ // RepoTags is empty
   151  			"docker-archive:testdata/docker-unnamed.tar.xz",
   152  			[]expected{{"@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951", "@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951"}},
   153  			true,
   154  		},
   155  		{ // RepoTags is a [docker.io/library/]name:latest, normalized to the short format.
   156  			"docker-archive:testdata/docker-name-only.tar.xz",
   157  			[]expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}},
   158  			true,
   159  		},
   160  		{ // RepoTags is a registry/name:latest
   161  			"docker-archive:testdata/docker-registry-name.tar.xz",
   162  			[]expected{{"example.com/empty:latest", "example.com/empty:latest"}},
   163  			true,
   164  		},
   165  		{ // RepoTags has multiple items for a single image
   166  			"docker-archive:testdata/docker-two-names.tar.xz",
   167  			[]expected{
   168  				{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"},
   169  				{"example.com/empty:latest", "example.com/empty:latest"},
   170  			},
   171  			true,
   172  		},
   173  		{ // Reference image by name in multi-image archive
   174  			"docker-archive:testdata/docker-two-images.tar.xz:example.com/empty:latest",
   175  			[]expected{
   176  				{"example.com/empty:latest", "example.com/empty:latest"},
   177  			},
   178  			true,
   179  		},
   180  		{ // Reference image by name in multi-image archive
   181  			"docker-archive:testdata/docker-two-images.tar.xz:example.com/empty/but:different",
   182  			[]expected{
   183  				{"example.com/empty/but:different", "example.com/empty/but:different"},
   184  			},
   185  			true,
   186  		},
   187  		{ // Reference image by index in multi-image archive
   188  			"docker-archive:testdata/docker-two-images.tar.xz:@0",
   189  			[]expected{
   190  				{"example.com/empty:latest", "example.com/empty:latest"},
   191  			},
   192  			true,
   193  		},
   194  		{ // Reference image by index in multi-image archive
   195  			"docker-archive:testdata/docker-two-images.tar.xz:@1",
   196  			[]expected{
   197  				{"example.com/empty/but:different", "example.com/empty/but:different"},
   198  			},
   199  			true,
   200  		},
   201  		{ // Reference entire multi-image archive must fail (more than one manifest)
   202  			"docker-archive:testdata/docker-two-images.tar.xz",
   203  			[]expected{},
   204  			true,
   205  		},
   206  
   207  		// == oci-archive:
   208  		{"oci-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist.
   209  		{"oci-archive:/dev/null", nil, false},                // Input exists but does not contain a manifest.
   210  		// FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged.
   211  		// { // No name annotation
   212  		// 	"oci-archive:testdata/oci-unnamed.tar.gz",
   213  		// 	[]expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}},
   214  		//  false,
   215  		// },
   216  		// { // Name is a name:latest (no normalization is defined).
   217  		// 	"oci-archive:testdata/oci-name-only.tar.gz",
   218  		// 	[]expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}},
   219  		//  false,
   220  		// },
   221  		// { // Name is a registry/name:latest
   222  		// 	"oci-archive:testdata/oci-registry-name.tar.gz",
   223  		// 	[]expected{{"example.com/empty:latest", "example.com/empty:latest"}},
   224  		//  false,
   225  		// },
   226  		// // Name exists, but is an invalid Docker reference; such names will fail when creating dstReference.
   227  		// {"oci-archive:testdata/oci-non-docker-name.tar.gz", nil, false},
   228  		// Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef.
   229  
   230  		// == dir:
   231  		{ // Absolute path
   232  			"dir:/dev/this-does-not-exist",
   233  			[]expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist:latest"}},
   234  			false,
   235  		},
   236  		{ // Relative path, single element.
   237  			"dir:this-does-not-exist",
   238  			[]expected{{"localhost/this-does-not-exist", "localhost/this-does-not-exist:latest"}},
   239  			false,
   240  		},
   241  		{ // Relative path, multiple elements.
   242  			"dir:testdata/this-does-not-exist",
   243  			[]expected{{"localhost/testdata/this-does-not-exist", "localhost/testdata/this-does-not-exist:latest"}},
   244  			false,
   245  		},
   246  
   247  		// == Others, notably:
   248  		// === docker:// (has ImageReference.DockerReference)
   249  		{ // Fully-specified input
   250  			"docker://docker.io/library/busybox:latest",
   251  			[]expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}},
   252  			false,
   253  		},
   254  		{ // Minimal form of the input
   255  			"docker://busybox",
   256  			[]expected{{"docker://busybox", "docker.io/library/busybox:latest"}},
   257  			false,
   258  		},
   259  
   260  		// === tarball: (as an example of what happens when ImageReference.DockerReference is nil).
   261  		// FIXME? This tries to parse "tarball:/dev/null" as a storageReference, and fails.
   262  		// (This is NOT an API promise that the results will continue to be this way.)
   263  		{"tarball:/dev/null", nil, false},
   264  	} {
   265  		srcRef, err := alltransports.ParseImageName(c.srcName)
   266  		require.NoError(t, err, c.srcName)
   267  
   268  		res, err := ir.pullGoalFromImageReference(context.Background(), srcRef, c.srcName, nil)
   269  		if len(c.expected) == 0 {
   270  			assert.Error(t, err, c.srcName)
   271  		} else {
   272  			require.NoError(t, err, c.srcName)
   273  			require.Len(t, res.refPairs, len(c.expected), c.srcName)
   274  			for i, e := range c.expected {
   275  				testDescription := fmt.Sprintf("%s #%d", c.srcName, i)
   276  				assert.Equal(t, e.image, res.refPairs[i].image, testDescription)
   277  				assert.Equal(t, transports.ImageName(srcRef), transports.ImageName(res.refPairs[i].srcRef), testDescription)
   278  				assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription)
   279  			}
   280  			assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName)
   281  		}
   282  	}
   283  }
   284  
   285  const registriesConfWithSearch = `unqualified-search-registries = ['example.com', 'docker.io']`
   286  
   287  func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
   288  	const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
   289  	type pullRefStrings struct{ image, srcRef, dstName string } // pullRefPair with string data only
   290  
   291  	registriesConf, err := ioutil.TempFile("", "TestPullGoalFromPossiblyUnqualifiedName")
   292  	require.NoError(t, err)
   293  	defer registriesConf.Close()
   294  	defer os.Remove(registriesConf.Name())
   295  
   296  	err = ioutil.WriteFile(registriesConf.Name(), []byte(registriesConfWithSearch), 0600)
   297  	require.NoError(t, err)
   298  
   299  	ir, cleanup := newTestRuntime(t)
   300  	defer cleanup()
   301  
   302  	sc := GetSystemContext("", "", false)
   303  
   304  	aliasesConf, err := ioutil.TempFile("", "short-name-aliases.conf")
   305  	require.NoError(t, err)
   306  	defer aliasesConf.Close()
   307  	defer os.Remove(aliasesConf.Name())
   308  	sc.UserShortNameAliasConfPath = aliasesConf.Name()
   309  	sc.SystemRegistriesConfPath = registriesConf.Name()
   310  
   311  	for _, c := range []struct {
   312  		input    string
   313  		expected []pullRefStrings
   314  	}{
   315  		{"#", nil}, // Clearly invalid.
   316  		{ // Fully-explicit docker.io, name-only.
   317  			"docker.io/library/busybox",
   318  			// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   319  			[]pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
   320  		},
   321  		{ // docker.io with implied /library/, name-only.
   322  			"docker.io/busybox",
   323  			// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   324  			[]pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
   325  		},
   326  		{ // Qualified example.com, name-only.
   327  			"example.com/ns/busybox",
   328  			[]pullRefStrings{{"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}},
   329  		},
   330  		{ // Qualified example.com, name:tag.
   331  			"example.com/ns/busybox:notlatest",
   332  			[]pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}},
   333  		},
   334  		{ // Qualified example.com, name@digest.
   335  			"example.com/ns/busybox" + digestSuffix,
   336  			[]pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix,
   337  				"example.com/ns/busybox" + digestSuffix}},
   338  		},
   339  		// Qualified example.com, name:tag@digest.  This code is happy to try, but .srcRef parsing currently rejects such input.
   340  		{"example.com/ns/busybox:notlatest" + digestSuffix, nil},
   341  		{ // Unqualified, single-name, name-only
   342  			"busybox",
   343  			[]pullRefStrings{
   344  				{"example.com/busybox:latest", "docker://example.com/busybox:latest", "example.com/busybox:latest"},
   345  				// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   346  				{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"},
   347  			},
   348  		},
   349  		{ // Unqualified, namespaced, name-only
   350  			"ns/busybox",
   351  			[]pullRefStrings{
   352  				{"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"},
   353  			},
   354  		},
   355  		{ // Unqualified, name:tag
   356  			"busybox:notlatest",
   357  			[]pullRefStrings{
   358  				{"example.com/busybox:notlatest", "docker://example.com/busybox:notlatest", "example.com/busybox:notlatest"},
   359  				// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   360  				{"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"},
   361  			},
   362  		},
   363  		{ // Unqualified, name@digest
   364  			"busybox" + digestSuffix,
   365  			[]pullRefStrings{
   366  				{"example.com/busybox" + digestSuffix, "docker://example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix},
   367  				// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   368  				{"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix},
   369  			},
   370  		},
   371  		// Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input.
   372  		{"busybox:notlatest" + digestSuffix, nil},
   373  	} {
   374  		res, err := ir.pullGoalFromPossiblyUnqualifiedName(sc, nil, c.input)
   375  		if len(c.expected) == 0 {
   376  			assert.Error(t, err, c.input)
   377  		} else {
   378  			assert.NoError(t, err, c.input)
   379  			for i, e := range c.expected {
   380  				testDescription := fmt.Sprintf("%s #%d (%v)", c.input, i, res.refPairs)
   381  				assert.Equal(t, e.image, res.refPairs[i].image, testDescription)
   382  				assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription)
   383  				assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription)
   384  			}
   385  			assert.False(t, res.pullAllPairs, c.input)
   386  		}
   387  	}
   388  }