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