github.com/docker/engine@v22.0.0-20211208180946-d456264580cf+incompatible/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 }