github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/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  			false,
   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  		{ // FIXME: Two images in a single archive - only the "first" one (whichever it is) is returned
   174  			// (and docker-archive: then refuses to read anything when the manifest has more than 1 item)
   175  			"docker-archive:testdata/docker-two-images.tar.xz",
   176  			[]expected{{"example.com/empty:latest", "example.com/empty:latest"}},
   177  			// "example.com/empty/but:different" exists but is ignored
   178  			true,
   179  		},
   180  
   181  		// == oci-archive:
   182  		{"oci-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist.
   183  		{"oci-archive:/dev/null", nil, false},                // Input exists but does not contain a manifest.
   184  		// FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged.
   185  		// { // No name annotation
   186  		// 	"oci-archive:testdata/oci-unnamed.tar.gz",
   187  		// 	[]expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}},
   188  		//  false,
   189  		// },
   190  		// { // Name is a name:latest (no normalization is defined).
   191  		// 	"oci-archive:testdata/oci-name-only.tar.gz",
   192  		// 	[]expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}},
   193  		//  false,
   194  		// },
   195  		// { // Name is a registry/name:latest
   196  		// 	"oci-archive:testdata/oci-registry-name.tar.gz",
   197  		// 	[]expected{{"example.com/empty:latest", "example.com/empty:latest"}},
   198  		//  false,
   199  		// },
   200  		// // Name exists, but is an invalid Docker reference; such names will fail when creating dstReference.
   201  		// {"oci-archive:testdata/oci-non-docker-name.tar.gz", nil, false},
   202  		// Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef.
   203  
   204  		// == dir:
   205  		{ // Absolute path
   206  			"dir:/dev/this-does-not-exist",
   207  			[]expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist:latest"}},
   208  			false,
   209  		},
   210  		{ // Relative path, single element.
   211  			"dir:this-does-not-exist",
   212  			[]expected{{"localhost/this-does-not-exist", "localhost/this-does-not-exist:latest"}},
   213  			false,
   214  		},
   215  		{ // Relative path, multiple elements.
   216  			"dir:testdata/this-does-not-exist",
   217  			[]expected{{"localhost/testdata/this-does-not-exist", "localhost/testdata/this-does-not-exist:latest"}},
   218  			false,
   219  		},
   220  
   221  		// == Others, notably:
   222  		// === docker:// (has ImageReference.DockerReference)
   223  		{ // Fully-specified input
   224  			"docker://docker.io/library/busybox:latest",
   225  			[]expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}},
   226  			false,
   227  		},
   228  		{ // Minimal form of the input
   229  			"docker://busybox",
   230  			[]expected{{"docker://busybox", "docker.io/library/busybox:latest"}},
   231  			false,
   232  		},
   233  
   234  		// === tarball: (as an example of what happens when ImageReference.DockerReference is nil).
   235  		// FIXME? This tries to parse "tarball:/dev/null" as a storageReference, and fails.
   236  		// (This is NOT an API promise that the results will continue to be this way.)
   237  		{"tarball:/dev/null", nil, false},
   238  	} {
   239  		srcRef, err := alltransports.ParseImageName(c.srcName)
   240  		require.NoError(t, err, c.srcName)
   241  
   242  		res, err := ir.pullGoalFromImageReference(context.Background(), srcRef, c.srcName, nil)
   243  		if len(c.expected) == 0 {
   244  			assert.Error(t, err, c.srcName)
   245  		} else {
   246  			require.NoError(t, err, c.srcName)
   247  			require.Len(t, res.refPairs, len(c.expected), c.srcName)
   248  			for i, e := range c.expected {
   249  				testDescription := fmt.Sprintf("%s #%d", c.srcName, i)
   250  				assert.Equal(t, e.image, res.refPairs[i].image, testDescription)
   251  				assert.Equal(t, srcRef, res.refPairs[i].srcRef, testDescription)
   252  				assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription)
   253  			}
   254  			assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName)
   255  			assert.False(t, res.usedSearchRegistries, c.srcName)
   256  			assert.Nil(t, res.searchedRegistries, c.srcName)
   257  		}
   258  	}
   259  }
   260  
   261  const registriesConfWithSearch = `[registries.search]
   262  registries = ['example.com', 'docker.io']
   263  `
   264  
   265  func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
   266  	const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
   267  	type pullRefStrings struct{ image, srcRef, dstName string } // pullRefPair with string data only
   268  
   269  	registriesConf, err := ioutil.TempFile("", "TestPullGoalFromPossiblyUnqualifiedName")
   270  	require.NoError(t, err)
   271  	defer registriesConf.Close()
   272  	defer os.Remove(registriesConf.Name())
   273  
   274  	err = ioutil.WriteFile(registriesConf.Name(), []byte(registriesConfWithSearch), 0600)
   275  	require.NoError(t, err)
   276  
   277  	ir, cleanup := newTestRuntime(t)
   278  	defer cleanup()
   279  
   280  	// Environment is per-process, so this looks very unsafe; actually it seems fine because tests are not
   281  	// run in parallel unless they opt in by calling t.Parallel().  So don’t do that.
   282  	oldRCP, hasRCP := os.LookupEnv("REGISTRIES_CONFIG_PATH")
   283  	defer func() {
   284  		if hasRCP {
   285  			os.Setenv("REGISTRIES_CONFIG_PATH", oldRCP)
   286  		} else {
   287  			os.Unsetenv("REGISTRIES_CONFIG_PATH")
   288  		}
   289  	}()
   290  	os.Setenv("REGISTRIES_CONFIG_PATH", registriesConf.Name())
   291  
   292  	for _, c := range []struct {
   293  		input                        string
   294  		expected                     []pullRefStrings
   295  		expectedUsedSearchRegistries bool
   296  	}{
   297  		{"#", nil, false}, // Clearly invalid.
   298  		{ // Fully-explicit docker.io, name-only.
   299  			"docker.io/library/busybox",
   300  			// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   301  			[]pullRefStrings{{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
   302  			false,
   303  		},
   304  		{ // docker.io with implied /library/, name-only.
   305  			"docker.io/busybox",
   306  			// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   307  			[]pullRefStrings{{"docker.io/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
   308  			false,
   309  		},
   310  		{ // Qualified example.com, name-only.
   311  			"example.com/ns/busybox",
   312  			[]pullRefStrings{{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}},
   313  			false,
   314  		},
   315  		{ // Qualified example.com, name:tag.
   316  			"example.com/ns/busybox:notlatest",
   317  			[]pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}},
   318  			false,
   319  		},
   320  		{ // Qualified example.com, name@digest.
   321  			"example.com/ns/busybox" + digestSuffix,
   322  			[]pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix,
   323  				"example.com/ns/busybox" + digestSuffix}},
   324  			false,
   325  		},
   326  		// Qualified example.com, name:tag@digest.  This code is happy to try, but .srcRef parsing currently rejects such input.
   327  		{"example.com/ns/busybox:notlatest" + digestSuffix, nil, false},
   328  		{ // Unqualified, single-name, name-only
   329  			"busybox",
   330  			[]pullRefStrings{
   331  				{"example.com/busybox", "docker://example.com/busybox:latest", "example.com/busybox:latest"},
   332  				// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   333  				{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"},
   334  			},
   335  			true,
   336  		},
   337  		{ // Unqualified, namespaced, name-only
   338  			"ns/busybox",
   339  			[]pullRefStrings{
   340  				{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"},
   341  			},
   342  			true,
   343  		},
   344  		{ // Unqualified, name:tag
   345  			"busybox:notlatest",
   346  			[]pullRefStrings{
   347  				{"example.com/busybox:notlatest", "docker://example.com/busybox:notlatest", "example.com/busybox:notlatest"},
   348  				// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   349  				{"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"},
   350  			},
   351  			true,
   352  		},
   353  		{ // Unqualified, name@digest
   354  			"busybox" + digestSuffix,
   355  			[]pullRefStrings{
   356  				{"example.com/busybox" + digestSuffix, "docker://example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix},
   357  				// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
   358  				{"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix},
   359  			},
   360  			true,
   361  		},
   362  		// Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input.
   363  		{"busybox:notlatest" + digestSuffix, nil, false},
   364  	} {
   365  		res, err := ir.pullGoalFromPossiblyUnqualifiedName(c.input)
   366  		if len(c.expected) == 0 {
   367  			assert.Error(t, err, c.input)
   368  		} else {
   369  			assert.NoError(t, err, c.input)
   370  			for i, e := range c.expected {
   371  				testDescription := fmt.Sprintf("%s #%d", c.input, i)
   372  				assert.Equal(t, e.image, res.refPairs[i].image, testDescription)
   373  				assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription)
   374  				assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription)
   375  			}
   376  			assert.False(t, res.pullAllPairs, c.input)
   377  			assert.Equal(t, c.expectedUsedSearchRegistries, res.usedSearchRegistries, c.input)
   378  			if !c.expectedUsedSearchRegistries {
   379  				assert.Nil(t, res.searchedRegistries, c.input)
   380  			} else {
   381  				assert.Equal(t, []string{"example.com", "docker.io"}, res.searchedRegistries, c.input)
   382  			}
   383  		}
   384  	}
   385  }