github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/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 }