github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/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  	"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.Encoded()
   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  	workingDir, err := os.Getwd()
   371  	assert.NilError(c, err)
   372  	absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth"))
   373  	assert.NilError(c, err)
   374  
   375  	osPath := os.Getenv("PATH")
   376  	testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute)
   377  	c.Setenv("PATH", testPath)
   378  
   379  	repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL)
   380  
   381  	tmp, err := os.MkdirTemp("", "integration-cli-")
   382  	assert.NilError(c, err)
   383  
   384  	externalAuthConfig := `{ "credsStore": "shell-test" }`
   385  
   386  	configPath := filepath.Join(tmp, "config.json")
   387  	err = os.WriteFile(configPath, []byte(externalAuthConfig), 0644)
   388  	assert.NilError(c, err)
   389  
   390  	dockerCmd(c, "--config", tmp, "login", "-u", s.reg.Username(), "-p", s.reg.Password(), privateRegistryURL)
   391  
   392  	b, err := os.ReadFile(configPath)
   393  	assert.NilError(c, err)
   394  	assert.Assert(c, !strings.Contains(string(b), "\"auth\":"))
   395  	dockerCmd(c, "--config", tmp, "tag", "busybox", repoName)
   396  	dockerCmd(c, "--config", tmp, "push", repoName)
   397  
   398  	dockerCmd(c, "--config", tmp, "logout", privateRegistryURL)
   399  	dockerCmd(c, "--config", tmp, "login", "-u", s.reg.Username(), "-p", s.reg.Password(), "https://"+privateRegistryURL)
   400  	dockerCmd(c, "--config", tmp, "pull", repoName)
   401  
   402  	// likewise push should work
   403  	repoName2 := fmt.Sprintf("%v/dockercli/busybox:nocreds", privateRegistryURL)
   404  	dockerCmd(c, "tag", repoName, repoName2)
   405  	dockerCmd(c, "--config", tmp, "push", repoName2)
   406  
   407  	// logout should work w scheme also because it will be stripped
   408  	dockerCmd(c, "--config", tmp, "logout", "https://"+privateRegistryURL)
   409  }
   410  
   411  func (s *DockerRegistryAuthHtpasswdSuite) TestPullWithExternalAuth(c *testing.T) {
   412  	workingDir, err := os.Getwd()
   413  	assert.NilError(c, err)
   414  	absolute, err := filepath.Abs(filepath.Join(workingDir, "fixtures", "auth"))
   415  	assert.NilError(c, err)
   416  
   417  	osPath := os.Getenv("PATH")
   418  	testPath := fmt.Sprintf("%s%c%s", osPath, filepath.ListSeparator, absolute)
   419  	c.Setenv("PATH", testPath)
   420  
   421  	repoName := fmt.Sprintf("%v/dockercli/busybox:authtest", privateRegistryURL)
   422  
   423  	tmp, err := os.MkdirTemp("", "integration-cli-")
   424  	assert.NilError(c, err)
   425  
   426  	externalAuthConfig := `{ "credsStore": "shell-test" }`
   427  
   428  	configPath := filepath.Join(tmp, "config.json")
   429  	err = os.WriteFile(configPath, []byte(externalAuthConfig), 0644)
   430  	assert.NilError(c, err)
   431  
   432  	dockerCmd(c, "--config", tmp, "login", "-u", s.reg.Username(), "-p", s.reg.Password(), privateRegistryURL)
   433  
   434  	b, err := os.ReadFile(configPath)
   435  	assert.NilError(c, err)
   436  	assert.Assert(c, !strings.Contains(string(b), "\"auth\":"))
   437  	dockerCmd(c, "--config", tmp, "tag", "busybox", repoName)
   438  	dockerCmd(c, "--config", tmp, "push", repoName)
   439  
   440  	dockerCmd(c, "--config", tmp, "pull", repoName)
   441  }
   442  
   443  // TestRunImplicitPullWithNoTag should pull implicitly only the default tag (latest)
   444  func (s *DockerRegistrySuite) TestRunImplicitPullWithNoTag(c *testing.T) {
   445  	testRequires(c, DaemonIsLinux)
   446  	repo := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL)
   447  	repoTag1 := fmt.Sprintf("%v:latest", repo)
   448  	repoTag2 := fmt.Sprintf("%v:t1", repo)
   449  	// tag the image and upload it to the private registry
   450  	dockerCmd(c, "tag", "busybox", repoTag1)
   451  	dockerCmd(c, "tag", "busybox", repoTag2)
   452  	dockerCmd(c, "push", repo)
   453  	dockerCmd(c, "rmi", repoTag1)
   454  	dockerCmd(c, "rmi", repoTag2)
   455  
   456  	out, _ := dockerCmd(c, "run", repo)
   457  	assert.Assert(c, strings.Contains(out, fmt.Sprintf("Unable to find image '%s:latest' locally", repo)))
   458  	// There should be only one line for repo, the one with repo:latest
   459  	outImageCmd, _ := dockerCmd(c, "images", repo)
   460  	splitOutImageCmd := strings.Split(strings.TrimSpace(outImageCmd), "\n")
   461  	assert.Equal(c, len(splitOutImageCmd), 2)
   462  }