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 }