gopkg.in/docker/docker.v20@v20.10.27/integration-cli/docker_cli_pull_local_test.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/docker/distribution"
    13  	"github.com/docker/distribution/manifest"
    14  	"github.com/docker/distribution/manifest/manifestlist"
    15  	"github.com/docker/distribution/manifest/schema2"
    16  	"github.com/docker/docker/integration-cli/cli/build"
    17  	digest "github.com/opencontainers/go-digest"
    18  	"gotest.tools/v3/assert"
    19  	"gotest.tools/v3/icmd"
    20  )
    21  
    22  // testPullImageWithAliases pulls a specific image tag and verifies that any aliases (i.e., other
    23  // tags for the same image) are not also pulled down.
    24  //
    25  // Ref: docker/docker#8141
    26  func testPullImageWithAliases(c *testing.T) {
    27  	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
    28  
    29  	var repos []string
    30  	for _, tag := range []string{"recent", "fresh"} {
    31  		repos = append(repos, fmt.Sprintf("%v:%v", repoName, tag))
    32  	}
    33  
    34  	// Tag and push the same image multiple times.
    35  	for _, repo := range repos {
    36  		dockerCmd(c, "tag", "busybox", repo)
    37  		dockerCmd(c, "push", repo)
    38  	}
    39  
    40  	// Clear local images store.
    41  	args := append([]string{"rmi"}, repos...)
    42  	dockerCmd(c, args...)
    43  
    44  	// Pull a single tag and verify it doesn't bring down all aliases.
    45  	dockerCmd(c, "pull", repos[0])
    46  	dockerCmd(c, "inspect", repos[0])
    47  	for _, repo := range repos[1:] {
    48  		_, _, err := dockerCmdWithError("inspect", repo)
    49  		assert.ErrorContains(c, err, "", "Image %v shouldn't have been pulled down", repo)
    50  	}
    51  }
    52  
    53  func (s *DockerRegistrySuite) TestPullImageWithAliases(c *testing.T) {
    54  	testPullImageWithAliases(c)
    55  }
    56  
    57  func (s *DockerSchema1RegistrySuite) TestPullImageWithAliases(c *testing.T) {
    58  	testPullImageWithAliases(c)
    59  }
    60  
    61  // testConcurrentPullWholeRepo pulls the same repo concurrently.
    62  func testConcurrentPullWholeRepo(c *testing.T) {
    63  	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
    64  
    65  	var repos []string
    66  	for _, tag := range []string{"recent", "fresh", "todays"} {
    67  		repo := fmt.Sprintf("%v:%v", repoName, tag)
    68  		buildImageSuccessfully(c, repo, build.WithDockerfile(fmt.Sprintf(`
    69  		    FROM busybox
    70  		    ENTRYPOINT ["/bin/echo"]
    71  		    ENV FOO foo
    72  		    ENV BAR bar
    73  		    CMD echo %s
    74  		`, repo)))
    75  		dockerCmd(c, "push", repo)
    76  		repos = append(repos, repo)
    77  	}
    78  
    79  	// Clear local images store.
    80  	args := append([]string{"rmi"}, repos...)
    81  	dockerCmd(c, args...)
    82  
    83  	// Run multiple re-pulls concurrently
    84  	numPulls := 3
    85  	results := make(chan error, numPulls)
    86  
    87  	for i := 0; i != numPulls; i++ {
    88  		go func() {
    89  			result := icmd.RunCommand(dockerBinary, "pull", "-a", repoName)
    90  			results <- result.Error
    91  		}()
    92  	}
    93  
    94  	// These checks are separate from the loop above because the check
    95  	// package is not goroutine-safe.
    96  	for i := 0; i != numPulls; i++ {
    97  		err := <-results
    98  		assert.NilError(c, err, "concurrent pull failed with error: %v", err)
    99  	}
   100  
   101  	// Ensure all tags were pulled successfully
   102  	for _, repo := range repos {
   103  		dockerCmd(c, "inspect", repo)
   104  		out, _ := dockerCmd(c, "run", "--rm", repo)
   105  		assert.Equal(c, strings.TrimSpace(out), "/bin/sh -c echo "+repo)
   106  	}
   107  }
   108  
   109  func (s *DockerRegistrySuite) TestConcurrentPullWholeRepo(c *testing.T) {
   110  	testConcurrentPullWholeRepo(c)
   111  }
   112  
   113  func (s *DockerSchema1RegistrySuite) TestConcurrentPullWholeRepo(c *testing.T) {
   114  	testConcurrentPullWholeRepo(c)
   115  }
   116  
   117  // testConcurrentFailingPull tries a concurrent pull that doesn't succeed.
   118  func testConcurrentFailingPull(c *testing.T) {
   119  	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
   120  
   121  	// Run multiple pulls concurrently
   122  	numPulls := 3
   123  	results := make(chan error, numPulls)
   124  
   125  	for i := 0; i != numPulls; i++ {
   126  		go func() {
   127  			result := icmd.RunCommand(dockerBinary, "pull", repoName+":asdfasdf")
   128  			results <- result.Error
   129  		}()
   130  	}
   131  
   132  	// These checks are separate from the loop above because the check
   133  	// package is not goroutine-safe.
   134  	for i := 0; i != numPulls; i++ {
   135  		err := <-results
   136  		assert.ErrorContains(c, err, "", "expected pull to fail")
   137  	}
   138  }
   139  
   140  func (s *DockerRegistrySuite) TestConcurrentFailingPull(c *testing.T) {
   141  	testConcurrentFailingPull(c)
   142  }
   143  
   144  func (s *DockerSchema1RegistrySuite) TestConcurrentFailingPull(c *testing.T) {
   145  	testConcurrentFailingPull(c)
   146  }
   147  
   148  // testConcurrentPullMultipleTags pulls multiple tags from the same repo
   149  // concurrently.
   150  func testConcurrentPullMultipleTags(c *testing.T) {
   151  	repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
   152  
   153  	var repos []string
   154  	for _, tag := range []string{"recent", "fresh", "todays"} {
   155  		repo := fmt.Sprintf("%v:%v", repoName, tag)
   156  		buildImageSuccessfully(c, repo, build.WithDockerfile(fmt.Sprintf(`
   157  		    FROM busybox
   158  		    ENTRYPOINT ["/bin/echo"]
   159  		    ENV FOO foo
   160  		    ENV BAR bar
   161  		    CMD echo %s
   162  		`, repo)))
   163  		dockerCmd(c, "push", repo)
   164  		repos = append(repos, repo)
   165  	}
   166  
   167  	// Clear local images store.
   168  	args := append([]string{"rmi"}, repos...)
   169  	dockerCmd(c, args...)
   170  
   171  	// Re-pull individual tags, in parallel
   172  	results := make(chan error, len(repos))
   173  
   174  	for _, repo := range repos {
   175  		go func(repo string) {
   176  			result := icmd.RunCommand(dockerBinary, "pull", repo)
   177  			results <- result.Error
   178  		}(repo)
   179  	}
   180  
   181  	// These checks are separate from the loop above because the check
   182  	// package is not goroutine-safe.
   183  	for range repos {
   184  		err := <-results
   185  		assert.NilError(c, err, "concurrent pull failed with error: %v", err)
   186  	}
   187  
   188  	// Ensure all tags were pulled successfully
   189  	for _, repo := range repos {
   190  		dockerCmd(c, "inspect", repo)
   191  		out, _ := dockerCmd(c, "run", "--rm", repo)
   192  		assert.Equal(c, strings.TrimSpace(out), "/bin/sh -c echo "+repo)
   193  	}
   194  }
   195  
   196  func (s *DockerRegistrySuite) TestConcurrentPullMultipleTags(c *testing.T) {
   197  	testConcurrentPullMultipleTags(c)
   198  }
   199  
   200  func (s *DockerSchema1RegistrySuite) TestConcurrentPullMultipleTags(c *testing.T) {
   201  	testConcurrentPullMultipleTags(c)
   202  }
   203  
   204  // testPullIDStability verifies that pushing an image and pulling it back
   205  // preserves the image ID.
   206  func testPullIDStability(c *testing.T) {
   207  	derivedImage := privateRegistryURL + "/dockercli/id-stability"
   208  	baseImage := "busybox"
   209  
   210  	buildImageSuccessfully(c, derivedImage, build.WithDockerfile(fmt.Sprintf(`
   211  	    FROM %s
   212  	    ENV derived true
   213  	    ENV asdf true
   214  	    RUN dd if=/dev/zero of=/file bs=1024 count=1024
   215  	    CMD echo %s
   216  	`, baseImage, derivedImage)))
   217  
   218  	originalID := getIDByName(c, derivedImage)
   219  	dockerCmd(c, "push", derivedImage)
   220  
   221  	// Pull
   222  	out, _ := dockerCmd(c, "pull", derivedImage)
   223  	if strings.Contains(out, "Pull complete") {
   224  		c.Fatalf("repull redownloaded a layer: %s", out)
   225  	}
   226  
   227  	derivedIDAfterPull := getIDByName(c, derivedImage)
   228  
   229  	if derivedIDAfterPull != originalID {
   230  		c.Fatal("image's ID unexpectedly changed after a repush/repull")
   231  	}
   232  
   233  	// Make sure the image runs correctly
   234  	out, _ = dockerCmd(c, "run", "--rm", derivedImage)
   235  	if strings.TrimSpace(out) != derivedImage {
   236  		c.Fatalf("expected %s; got %s", derivedImage, out)
   237  	}
   238  
   239  	// Confirm that repushing and repulling does not change the computed ID
   240  	dockerCmd(c, "push", derivedImage)
   241  	dockerCmd(c, "rmi", derivedImage)
   242  	dockerCmd(c, "pull", derivedImage)
   243  
   244  	derivedIDAfterPull = getIDByName(c, derivedImage)
   245  
   246  	if derivedIDAfterPull != originalID {
   247  		c.Fatal("image's ID unexpectedly changed after a repush/repull")
   248  	}
   249  
   250  	// Make sure the image still runs
   251  	out, _ = dockerCmd(c, "run", "--rm", derivedImage)
   252  	if strings.TrimSpace(out) != derivedImage {
   253  		c.Fatalf("expected %s; got %s", derivedImage, out)
   254  	}
   255  }
   256  
   257  func (s *DockerRegistrySuite) TestPullIDStability(c *testing.T) {
   258  	testPullIDStability(c)
   259  }
   260  
   261  func (s *DockerSchema1RegistrySuite) TestPullIDStability(c *testing.T) {
   262  	testPullIDStability(c)
   263  }
   264  
   265  // #21213
   266  func testPullNoLayers(c *testing.T) {
   267  	repoName := fmt.Sprintf("%v/dockercli/scratch", privateRegistryURL)
   268  
   269  	buildImageSuccessfully(c, repoName, build.WithDockerfile(`
   270  	FROM scratch
   271  	ENV foo bar`))
   272  	dockerCmd(c, "push", repoName)
   273  	dockerCmd(c, "rmi", repoName)
   274  	dockerCmd(c, "pull", repoName)
   275  }
   276  
   277  func (s *DockerRegistrySuite) TestPullNoLayers(c *testing.T) {
   278  	testPullNoLayers(c)
   279  }
   280  
   281  func (s *DockerSchema1RegistrySuite) TestPullNoLayers(c *testing.T) {
   282  	testPullNoLayers(c)
   283  }
   284  
   285  func (s *DockerRegistrySuite) TestPullManifestList(c *testing.T) {
   286  	testRequires(c, NotArm)
   287  	pushDigest, err := setupImage(c)
   288  	assert.NilError(c, err, "error setting up image")
   289  
   290  	// Inject a manifest list into the registry
   291  	manifestList := &manifestlist.ManifestList{
   292  		Versioned: manifest.Versioned{
   293  			SchemaVersion: 2,
   294  			MediaType:     manifestlist.MediaTypeManifestList,
   295  		},
   296  		Manifests: []manifestlist.ManifestDescriptor{
   297  			{
   298  				Descriptor: distribution.Descriptor{
   299  					Digest:    "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b",
   300  					Size:      3253,
   301  					MediaType: schema2.MediaTypeManifest,
   302  				},
   303  				Platform: manifestlist.PlatformSpec{
   304  					Architecture: "bogus_arch",
   305  					OS:           "bogus_os",
   306  				},
   307  			},
   308  			{
   309  				Descriptor: distribution.Descriptor{
   310  					Digest:    pushDigest,
   311  					Size:      3253,
   312  					MediaType: schema2.MediaTypeManifest,
   313  				},
   314  				Platform: manifestlist.PlatformSpec{
   315  					Architecture: runtime.GOARCH,
   316  					OS:           runtime.GOOS,
   317  				},
   318  			},
   319  		},
   320  	}
   321  
   322  	manifestListJSON, err := json.MarshalIndent(manifestList, "", "   ")
   323  	assert.NilError(c, err, "error marshalling manifest list")
   324  
   325  	manifestListDigest := digest.FromBytes(manifestListJSON)
   326  	hexDigest := manifestListDigest.Hex()
   327  
   328  	registryV2Path := s.reg.Path()
   329  
   330  	// Write manifest list to blob store
   331  	blobDir := filepath.Join(registryV2Path, "blobs", "sha256", hexDigest[:2], hexDigest)
   332  	err = os.MkdirAll(blobDir, 0755)
   333  	assert.NilError(c, err, "error creating blob dir")
   334  	blobPath := filepath.Join(blobDir, "data")
   335  	err = os.WriteFile(blobPath, manifestListJSON, 0644)
   336  	assert.NilError(c, err, "error writing manifest list")
   337  
   338  	// Add to revision store
   339  	revisionDir := filepath.Join(registryV2Path, "repositories", remoteRepoName, "_manifests", "revisions", "sha256", hexDigest)
   340  	err = os.Mkdir(revisionDir, 0755)
   341  	assert.Assert(c, err == nil, "error creating revision dir")
   342  	revisionPath := filepath.Join(revisionDir, "link")
   343  	err = os.WriteFile(revisionPath, []byte(manifestListDigest.String()), 0644)
   344  	assert.Assert(c, err == nil, "error writing revision link")
   345  
   346  	// Update tag
   347  	tagPath := filepath.Join(registryV2Path, "repositories", remoteRepoName, "_manifests", "tags", "latest", "current", "link")
   348  	err = os.WriteFile(tagPath, []byte(manifestListDigest.String()), 0644)
   349  	assert.NilError(c, err, "error writing tag link")
   350  
   351  	// Verify that the image can be pulled through the manifest list.
   352  	out, _ := dockerCmd(c, "pull", repoName)
   353  
   354  	// The pull output includes "Digest: <digest>", so find that
   355  	matches := digestRegex.FindStringSubmatch(out)
   356  	assert.Equal(c, len(matches), 2, fmt.Sprintf("unable to parse digest from pull output: %s", out))
   357  	pullDigest := matches[1]
   358  
   359  	// Make sure the pushed and pull digests match
   360  	assert.Equal(c, manifestListDigest.String(), pullDigest)
   361  
   362  	// Was the image actually created?
   363  	dockerCmd(c, "inspect", repoName)
   364  
   365  	dockerCmd(c, "rmi", repoName)
   366  }
   367  
   368  // #23100
   369  func (s *DockerRegistryAuthHtpasswdSuite) TestPullWithExternalAuthLoginWithScheme(c *testing.T) {
   370  	osPath := os.Getenv("PATH")
   371  	defer os.Setenv("PATH", osPath)
   372  
   373  	workingDir, err := os.Getwd()
   374  	assert.NilError(c, err)
   375  	absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth"))
   376  	assert.NilError(c, err)
   377  	testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute)
   378  
   379  	os.Setenv("PATH", testPath)
   380  
   381  	repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL)
   382  
   383  	tmp, err := os.MkdirTemp("", "integration-cli-")
   384  	assert.NilError(c, err)
   385  
   386  	externalAuthConfig := `{ "credsStore": "shell-test" }`
   387  
   388  	configPath := filepath.Join(tmp, "config.json")
   389  	err = os.WriteFile(configPath, []byte(externalAuthConfig), 0644)
   390  	assert.NilError(c, err)
   391  
   392  	dockerCmd(c, "--config", tmp, "login", "-u", s.reg.Username(), "-p", s.reg.Password(), privateRegistryURL)
   393  
   394  	b, err := os.ReadFile(configPath)
   395  	assert.NilError(c, err)
   396  	assert.Assert(c, !strings.Contains(string(b), "\"auth\":"))
   397  	dockerCmd(c, "--config", tmp, "tag", "busybox", repoName)
   398  	dockerCmd(c, "--config", tmp, "push", repoName)
   399  
   400  	dockerCmd(c, "--config", tmp, "logout", privateRegistryURL)
   401  	dockerCmd(c, "--config", tmp, "login", "-u", s.reg.Username(), "-p", s.reg.Password(), "https://"+privateRegistryURL)
   402  	dockerCmd(c, "--config", tmp, "pull", repoName)
   403  
   404  	// likewise push should work
   405  	repoName2 := fmt.Sprintf("%v/dockercli/busybox:nocreds", privateRegistryURL)
   406  	dockerCmd(c, "tag", repoName, repoName2)
   407  	dockerCmd(c, "--config", tmp, "push", repoName2)
   408  
   409  	// logout should work w scheme also because it will be stripped
   410  	dockerCmd(c, "--config", tmp, "logout", "https://"+privateRegistryURL)
   411  }
   412  
   413  func (s *DockerRegistryAuthHtpasswdSuite) TestPullWithExternalAuth(c *testing.T) {
   414  	osPath := os.Getenv("PATH")
   415  	defer os.Setenv("PATH", osPath)
   416  
   417  	workingDir, err := os.Getwd()
   418  	assert.NilError(c, err)
   419  	absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth"))
   420  	assert.NilError(c, err)
   421  	testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute)
   422  
   423  	os.Setenv("PATH", testPath)
   424  
   425  	repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL)
   426  
   427  	tmp, err := os.MkdirTemp("", "integration-cli-")
   428  	assert.NilError(c, err)
   429  
   430  	externalAuthConfig := `{ "credsStore": "shell-test" }`
   431  
   432  	configPath := filepath.Join(tmp, "config.json")
   433  	err = os.WriteFile(configPath, []byte(externalAuthConfig), 0644)
   434  	assert.NilError(c, err)
   435  
   436  	dockerCmd(c, "--config", tmp, "login", "-u", s.reg.Username(), "-p", s.reg.Password(), privateRegistryURL)
   437  
   438  	b, err := os.ReadFile(configPath)
   439  	assert.NilError(c, err)
   440  	assert.Assert(c, !strings.Contains(string(b), "\"auth\":"))
   441  	dockerCmd(c, "--config", tmp, "tag", "busybox", repoName)
   442  	dockerCmd(c, "--config", tmp, "push", repoName)
   443  
   444  	dockerCmd(c, "--config", tmp, "pull", repoName)
   445  }
   446  
   447  // TestRunImplicitPullWithNoTag should pull implicitly only the default tag (latest)
   448  func (s *DockerRegistrySuite) TestRunImplicitPullWithNoTag(c *testing.T) {
   449  	testRequires(c, DaemonIsLinux)
   450  	repo := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
   451  	repoTag1 := fmt.Sprintf("%v:latest", repo)
   452  	repoTag2 := fmt.Sprintf("%v:t1", repo)
   453  	// tag the image and upload it to the private registry
   454  	dockerCmd(c, "tag", "busybox", repoTag1)
   455  	dockerCmd(c, "tag", "busybox", repoTag2)
   456  	dockerCmd(c, "push", repo)
   457  	dockerCmd(c, "rmi", repoTag1)
   458  	dockerCmd(c, "rmi", repoTag2)
   459  
   460  	out, _ := dockerCmd(c, "run", repo)
   461  	assert.Assert(c, strings.Contains(out, fmt.Sprintf("Unable to find image '%s:latest' locally", repo)))
   462  	// There should be only one line for repo, the one with repo:latest
   463  	outImageCmd, _ := dockerCmd(c, "images", repo)
   464  	splitOutImageCmd := strings.Split(strings.TrimSpace(outImageCmd), "\n")
   465  	assert.Equal(c, len(splitOutImageCmd), 2)
   466  }