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