github.com/afbjorklund/moby@v20.10.5+incompatible/integration-cli/docker_cli_pull_test.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "regexp" 6 "strings" 7 "sync" 8 "testing" 9 "time" 10 11 digest "github.com/opencontainers/go-digest" 12 "gotest.tools/v3/assert" 13 is "gotest.tools/v3/assert/cmp" 14 ) 15 16 // TestPullFromCentralRegistry pulls an image from the central registry and verifies that the client 17 // prints all expected output. 18 func (s *DockerHubPullSuite) TestPullFromCentralRegistry(c *testing.T) { 19 testRequires(c, DaemonIsLinux) 20 out := s.Cmd(c, "pull", "hello-world") 21 defer deleteImages("hello-world") 22 23 assert.Assert(c, strings.Contains(out, "Using default tag: latest"), "expected the 'latest' tag to be automatically assumed") 24 assert.Assert(c, strings.Contains(out, "Pulling from library/hello-world"), "expected the 'library/' prefix to be automatically assumed") 25 assert.Assert(c, strings.Contains(out, "Downloaded newer image for hello-world:latest")) 26 27 matches := regexp.MustCompile(`Digest: (.+)\n`).FindAllStringSubmatch(out, -1) 28 assert.Equal(c, len(matches), 1, "expected exactly one image digest in the output") 29 assert.Equal(c, len(matches[0]), 2, "unexpected number of submatches for the digest") 30 _, err := digest.Parse(matches[0][1]) 31 assert.NilError(c, err, "invalid digest %q in output", matches[0][1]) 32 33 // We should have a single entry in images. 34 img := strings.TrimSpace(s.Cmd(c, "images")) 35 splitImg := strings.Split(img, "\n") 36 assert.Equal(c, len(splitImg), 2) 37 match, _ := regexp.MatchString(`hello-world\s+latest.*?`, splitImg[1]) 38 assert.Assert(c, match, "invalid output for `docker images` (expected image and tag name)") 39 } 40 41 // TestPullNonExistingImage pulls non-existing images from the central registry, with different 42 // combinations of implicit tag and library prefix. 43 func (s *DockerHubPullSuite) TestPullNonExistingImage(c *testing.T) { 44 testRequires(c, DaemonIsLinux) 45 46 type entry struct { 47 repo string 48 alias string 49 tag string 50 } 51 52 entries := []entry{ 53 {"asdfasdf", "asdfasdf", "foobar"}, 54 {"asdfasdf", "library/asdfasdf", "foobar"}, 55 {"asdfasdf", "asdfasdf", ""}, 56 {"asdfasdf", "asdfasdf", "latest"}, 57 {"asdfasdf", "library/asdfasdf", ""}, 58 {"asdfasdf", "library/asdfasdf", "latest"}, 59 } 60 61 // The option field indicates "-a" or not. 62 type record struct { 63 e entry 64 option string 65 out string 66 err error 67 } 68 69 // Execute 'docker pull' in parallel, pass results (out, err) and 70 // necessary information ("-a" or not, and the image name) to channel. 71 var group sync.WaitGroup 72 recordChan := make(chan record, len(entries)*2) 73 for _, e := range entries { 74 group.Add(1) 75 go func(e entry) { 76 defer group.Done() 77 repoName := e.alias 78 if e.tag != "" { 79 repoName += ":" + e.tag 80 } 81 out, err := s.CmdWithError("pull", repoName) 82 recordChan <- record{e, "", out, err} 83 }(e) 84 if e.tag == "" { 85 // pull -a on a nonexistent registry should fall back as well 86 group.Add(1) 87 go func(e entry) { 88 defer group.Done() 89 out, err := s.CmdWithError("pull", "-a", e.alias) 90 recordChan <- record{e, "-a", out, err} 91 }(e) 92 } 93 } 94 95 // Wait for completion 96 group.Wait() 97 close(recordChan) 98 99 // Process the results (out, err). 100 for record := range recordChan { 101 if len(record.option) == 0 { 102 assert.ErrorContains(c, record.err, "", "expected non-zero exit status when pulling non-existing image: %s", record.out) 103 assert.Assert(c, strings.Contains(record.out, fmt.Sprintf("pull access denied for %s, repository does not exist or may require 'docker login'", record.e.repo)), "expected image not found error messages") 104 } else { 105 // pull -a on a nonexistent registry should fall back as well 106 assert.ErrorContains(c, record.err, "", "expected non-zero exit status when pulling non-existing image: %s", record.out) 107 assert.Assert(c, strings.Contains(record.out, fmt.Sprintf("pull access denied for %s, repository does not exist or may require 'docker login'", record.e.repo)), "expected image not found error messages") 108 assert.Assert(c, !strings.Contains(record.out, "unauthorized"), `message should not contain "unauthorized"`) 109 } 110 } 111 112 } 113 114 // TestPullFromCentralRegistryImplicitRefParts pulls an image from the central registry and verifies 115 // that pulling the same image with different combinations of implicit elements of the image 116 // reference (tag, repository, central registry url, ...) doesn't trigger a new pull nor leads to 117 // multiple images. 118 func (s *DockerHubPullSuite) TestPullFromCentralRegistryImplicitRefParts(c *testing.T) { 119 testRequires(c, DaemonIsLinux) 120 121 // Pull hello-world from v2 122 pullFromV2 := func(ref string) (int, string) { 123 out := s.Cmd(c, "pull", "hello-world") 124 v1Retries := 0 125 for strings.Contains(out, "this image was pulled from a legacy registry") { 126 // Some network errors may cause fallbacks to the v1 127 // protocol, which would violate the test's assumption 128 // that it will get the same images. To make the test 129 // more robust against these network glitches, allow a 130 // few retries if we end up with a v1 pull. 131 132 if v1Retries > 2 { 133 c.Fatalf("too many v1 fallback incidents when pulling %s", ref) 134 } 135 136 s.Cmd(c, "rmi", ref) 137 out = s.Cmd(c, "pull", ref) 138 139 v1Retries++ 140 } 141 142 return v1Retries, out 143 } 144 145 pullFromV2("hello-world") 146 defer deleteImages("hello-world") 147 148 s.Cmd(c, "tag", "hello-world", "hello-world-backup") 149 150 for _, ref := range []string{ 151 "hello-world", 152 "hello-world:latest", 153 "library/hello-world", 154 "library/hello-world:latest", 155 "docker.io/library/hello-world", 156 "index.docker.io/library/hello-world", 157 } { 158 var out string 159 for { 160 var v1Retries int 161 v1Retries, out = pullFromV2(ref) 162 163 // Keep repeating the test case until we don't hit a v1 164 // fallback case. We won't get the right "Image is up 165 // to date" message if the local image was replaced 166 // with one pulled from v1. 167 if v1Retries == 0 { 168 break 169 } 170 s.Cmd(c, "rmi", ref) 171 s.Cmd(c, "tag", "hello-world-backup", "hello-world") 172 } 173 assert.Assert(c, strings.Contains(out, "Image is up to date for hello-world:latest")) 174 } 175 176 s.Cmd(c, "rmi", "hello-world-backup") 177 178 // We should have a single entry in images. 179 img := strings.TrimSpace(s.Cmd(c, "images")) 180 splitImg := strings.Split(img, "\n") 181 assert.Equal(c, len(splitImg), 2) 182 match, _ := regexp.MatchString(`hello-world\s+latest.*?`, splitImg[1]) 183 assert.Assert(c, match, "invalid output for `docker images` (expected image and tag name)") 184 } 185 186 // TestPullScratchNotAllowed verifies that pulling 'scratch' is rejected. 187 func (s *DockerHubPullSuite) TestPullScratchNotAllowed(c *testing.T) { 188 testRequires(c, DaemonIsLinux) 189 out, err := s.CmdWithError("pull", "scratch") 190 assert.ErrorContains(c, err, "", "expected pull of scratch to fail") 191 assert.Assert(c, strings.Contains(out, "'scratch' is a reserved name")) 192 assert.Assert(c, !strings.Contains(out, "Pulling repository scratch")) 193 } 194 195 // TestPullAllTagsFromCentralRegistry pulls using `all-tags` for a given image and verifies that it 196 // results in more images than a naked pull. 197 func (s *DockerHubPullSuite) TestPullAllTagsFromCentralRegistry(c *testing.T) { 198 testRequires(c, DaemonIsLinux) 199 s.Cmd(c, "pull", "dockercore/engine-pull-all-test-fixture") 200 outImageCmd := s.Cmd(c, "images", "dockercore/engine-pull-all-test-fixture") 201 splitOutImageCmd := strings.Split(strings.TrimSpace(outImageCmd), "\n") 202 assert.Equal(c, len(splitOutImageCmd), 2) 203 204 s.Cmd(c, "pull", "--all-tags=true", "dockercore/engine-pull-all-test-fixture") 205 outImageAllTagCmd := s.Cmd(c, "images", "dockercore/engine-pull-all-test-fixture") 206 linesCount := strings.Count(outImageAllTagCmd, "\n") 207 assert.Assert(c, linesCount > 2, "pulling all tags should provide more than two images, got %s", outImageAllTagCmd) 208 209 // Verify that the line for 'dockercore/engine-pull-all-test-fixture:latest' is left unchanged. 210 var latestLine string 211 for _, line := range strings.Split(outImageAllTagCmd, "\n") { 212 if strings.HasPrefix(line, "dockercore/engine-pull-all-test-fixture") && strings.Contains(line, "latest") { 213 latestLine = line 214 break 215 } 216 } 217 assert.Assert(c, latestLine != "", "no entry for dockercore/engine-pull-all-test-fixture:latest found after pulling all tags") 218 219 splitLatest := strings.Fields(latestLine) 220 splitCurrent := strings.Fields(splitOutImageCmd[1]) 221 222 // Clear relative creation times, since these can easily change between 223 // two invocations of "docker images". Without this, the test can fail 224 // like this: 225 // ... obtained []string = []string{"busybox", "latest", "d9551b4026f0", "27", "minutes", "ago", "1.113", "MB"} 226 // ... expected []string = []string{"busybox", "latest", "d9551b4026f0", "26", "minutes", "ago", "1.113", "MB"} 227 splitLatest[3] = "" 228 splitLatest[4] = "" 229 splitLatest[5] = "" 230 splitCurrent[3] = "" 231 splitCurrent[4] = "" 232 splitCurrent[5] = "" 233 234 assert.Assert(c, is.DeepEqual(splitLatest, splitCurrent), "dockercore/engine-pull-all-test-fixture:latest was changed after pulling all tags") 235 } 236 237 // TestPullClientDisconnect kills the client during a pull operation and verifies that the operation 238 // gets cancelled. 239 // 240 // Ref: docker/docker#15589 241 func (s *DockerHubPullSuite) TestPullClientDisconnect(c *testing.T) { 242 testRequires(c, DaemonIsLinux) 243 repoName := "hello-world:latest" 244 245 pullCmd := s.MakeCmd("pull", repoName) 246 stdout, err := pullCmd.StdoutPipe() 247 assert.NilError(c, err) 248 err = pullCmd.Start() 249 assert.NilError(c, err) 250 go pullCmd.Wait() 251 252 // Cancel as soon as we get some output. 253 buf := make([]byte, 10) 254 _, err = stdout.Read(buf) 255 assert.NilError(c, err) 256 257 err = pullCmd.Process.Kill() 258 assert.NilError(c, err) 259 260 time.Sleep(2 * time.Second) 261 _, err = s.CmdWithError("inspect", repoName) 262 assert.ErrorContains(c, err, "", "image was pulled after client disconnected") 263 } 264 265 // Regression test for https://github.com/docker/docker/issues/26429 266 func (s *DockerSuite) TestPullLinuxImageFailsOnWindows(c *testing.T) { 267 testRequires(c, DaemonIsWindows, Network) 268 _, _, err := dockerCmdWithError("pull", "ubuntu") 269 assert.ErrorContains(c, err, "no matching manifest for windows") 270 } 271 272 // Regression test for https://github.com/docker/docker/issues/28892 273 func (s *DockerSuite) TestPullWindowsImageFailsOnLinux(c *testing.T) { 274 testRequires(c, DaemonIsLinux, Network) 275 _, _, err := dockerCmdWithError("pull", "mcr.microsoft.com/windows/servercore:ltsc2019") 276 assert.ErrorContains(c, err, "no matching manifest for linux") 277 }