github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/integration-cli/docker_cli_push_test.go (about) 1 package main 2 3 import ( 4 "archive/tar" 5 "fmt" 6 "io/ioutil" 7 "net/http" 8 "net/http/httptest" 9 "os" 10 "strings" 11 "sync" 12 "testing" 13 14 "github.com/docker/distribution/reference" 15 "github.com/docker/docker/api/types/versions" 16 "github.com/docker/docker/integration-cli/cli/build" 17 "gotest.tools/v3/assert" 18 "gotest.tools/v3/icmd" 19 ) 20 21 // Pushing an image to a private registry. 22 func testPushBusyboxImage(c *testing.T) { 23 repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 24 // tag the image to upload it to the private registry 25 dockerCmd(c, "tag", "busybox", repoName) 26 // push the image to the registry 27 dockerCmd(c, "push", repoName) 28 } 29 30 func (s *DockerRegistrySuite) TestPushBusyboxImage(c *testing.T) { 31 testPushBusyboxImage(c) 32 } 33 34 func (s *DockerSchema1RegistrySuite) TestPushBusyboxImage(c *testing.T) { 35 testPushBusyboxImage(c) 36 } 37 38 // pushing an image without a prefix should throw an error 39 func (s *DockerSuite) TestPushUnprefixedRepo(c *testing.T) { 40 out, _, err := dockerCmdWithError("push", "busybox") 41 assert.ErrorContains(c, err, "", "pushing an unprefixed repo didn't result in a non-zero exit status: %s", out) 42 } 43 44 func testPushUntagged(c *testing.T) { 45 repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 46 expected := "An image does not exist locally with the tag" 47 48 out, _, err := dockerCmdWithError("push", repoName) 49 assert.ErrorContains(c, err, "", "pushing the image to the private registry should have failed: output %q", out) 50 assert.Assert(c, strings.Contains(out, expected), "pushing the image failed") 51 } 52 53 func (s *DockerRegistrySuite) TestPushUntagged(c *testing.T) { 54 testPushUntagged(c) 55 } 56 57 func (s *DockerSchema1RegistrySuite) TestPushUntagged(c *testing.T) { 58 testPushUntagged(c) 59 } 60 61 func testPushBadTag(c *testing.T) { 62 repoName := fmt.Sprintf("%v/dockercli/busybox:latest", privateRegistryURL) 63 expected := "does not exist" 64 65 out, _, err := dockerCmdWithError("push", repoName) 66 assert.ErrorContains(c, err, "", "pushing the image to the private registry should have failed: output %q", out) 67 assert.Assert(c, strings.Contains(out, expected), "pushing the image failed") 68 } 69 70 func (s *DockerRegistrySuite) TestPushBadTag(c *testing.T) { 71 testPushBadTag(c) 72 } 73 74 func (s *DockerSchema1RegistrySuite) TestPushBadTag(c *testing.T) { 75 testPushBadTag(c) 76 } 77 78 func testPushMultipleTags(c *testing.T) { 79 repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 80 repoTag1 := fmt.Sprintf("%v/dockercli/busybox:t1", privateRegistryURL) 81 repoTag2 := fmt.Sprintf("%v/dockercli/busybox:t2", privateRegistryURL) 82 // tag the image and upload it to the private registry 83 dockerCmd(c, "tag", "busybox", repoTag1) 84 dockerCmd(c, "tag", "busybox", repoTag2) 85 86 args := []string{"push"} 87 if versions.GreaterThanOrEqualTo(DockerCLIVersion(c), "20.10.0") { 88 // 20.10 CLI removed implicit push all tags and requires the "--all" flag 89 args = append(args, "--all-tags") 90 } 91 args = append(args, repoName) 92 93 dockerCmd(c, args...) 94 95 imageAlreadyExists := ": Image already exists" 96 97 // Ensure layer list is equivalent for repoTag1 and repoTag2 98 out1, _ := dockerCmd(c, "push", repoTag1) 99 var out1Lines []string 100 for _, outputLine := range strings.Split(out1, "\n") { 101 if strings.Contains(outputLine, imageAlreadyExists) { 102 out1Lines = append(out1Lines, outputLine) 103 } 104 } 105 106 out2, _ := dockerCmd(c, "push", repoTag2) 107 var out2Lines []string 108 for _, outputLine := range strings.Split(out2, "\n") { 109 if strings.Contains(outputLine, imageAlreadyExists) { 110 out2Lines = append(out2Lines, outputLine) 111 } 112 } 113 assert.DeepEqual(c, out1Lines, out2Lines) 114 } 115 116 func (s *DockerRegistrySuite) TestPushMultipleTags(c *testing.T) { 117 testPushMultipleTags(c) 118 } 119 120 func (s *DockerSchema1RegistrySuite) TestPushMultipleTags(c *testing.T) { 121 testPushMultipleTags(c) 122 } 123 124 func testPushEmptyLayer(c *testing.T) { 125 repoName := fmt.Sprintf("%v/dockercli/emptylayer", privateRegistryURL) 126 emptyTarball, err := ioutil.TempFile("", "empty_tarball") 127 assert.NilError(c, err, "Unable to create test file") 128 129 tw := tar.NewWriter(emptyTarball) 130 err = tw.Close() 131 assert.NilError(c, err, "Error creating empty tarball") 132 133 freader, err := os.Open(emptyTarball.Name()) 134 assert.NilError(c, err, "Could not open test tarball") 135 defer freader.Close() 136 137 icmd.RunCmd(icmd.Cmd{ 138 Command: []string{dockerBinary, "import", "-", repoName}, 139 Stdin: freader, 140 }).Assert(c, icmd.Success) 141 142 // Now verify we can push it 143 out, _, err := dockerCmdWithError("push", repoName) 144 assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out) 145 } 146 147 func (s *DockerRegistrySuite) TestPushEmptyLayer(c *testing.T) { 148 testPushEmptyLayer(c) 149 } 150 151 func (s *DockerSchema1RegistrySuite) TestPushEmptyLayer(c *testing.T) { 152 testPushEmptyLayer(c) 153 } 154 155 // testConcurrentPush pushes multiple tags to the same repo 156 // concurrently. 157 func testConcurrentPush(c *testing.T) { 158 repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 159 160 var repos []string 161 for _, tag := range []string{"push1", "push2", "push3"} { 162 repo := fmt.Sprintf("%v:%v", repoName, tag) 163 buildImageSuccessfully(c, repo, build.WithDockerfile(fmt.Sprintf(` 164 FROM busybox 165 ENTRYPOINT ["/bin/echo"] 166 ENV FOO foo 167 ENV BAR bar 168 CMD echo %s 169 `, repo))) 170 repos = append(repos, repo) 171 } 172 173 // Push 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, "push", repo) 179 results <- result.Error 180 }(repo) 181 } 182 183 for range repos { 184 err := <-results 185 assert.NilError(c, err, "concurrent push failed with error: %v", err) 186 } 187 188 // Clear local images store. 189 args := append([]string{"rmi"}, repos...) 190 dockerCmd(c, args...) 191 192 // Re-pull and run individual tags, to make sure pushes succeeded 193 for _, repo := range repos { 194 dockerCmd(c, "pull", repo) 195 dockerCmd(c, "inspect", repo) 196 out, _ := dockerCmd(c, "run", "--rm", repo) 197 assert.Equal(c, strings.TrimSpace(out), "/bin/sh -c echo "+repo) 198 } 199 } 200 201 func (s *DockerRegistrySuite) TestConcurrentPush(c *testing.T) { 202 testConcurrentPush(c) 203 } 204 205 func (s *DockerSchema1RegistrySuite) TestConcurrentPush(c *testing.T) { 206 testConcurrentPush(c) 207 } 208 209 func (s *DockerRegistrySuite) TestCrossRepositoryLayerPush(c *testing.T) { 210 sourceRepoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 211 // tag the image to upload it to the private registry 212 dockerCmd(c, "tag", "busybox", sourceRepoName) 213 // push the image to the registry 214 out1, _, err := dockerCmdWithError("push", sourceRepoName) 215 assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out1) 216 // ensure that none of the layers were mounted from another repository during push 217 assert.Assert(c, !strings.Contains(out1, "Mounted from")) 218 219 digest1 := reference.DigestRegexp.FindString(out1) 220 assert.Assert(c, len(digest1) > 0, "no digest found for pushed manifest") 221 222 destRepoName := fmt.Sprintf("%v/dockercli/crossrepopush", privateRegistryURL) 223 // retag the image to upload the same layers to another repo in the same registry 224 dockerCmd(c, "tag", "busybox", destRepoName) 225 // push the image to the registry 226 out2, _, err := dockerCmdWithError("push", destRepoName) 227 assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out2) 228 229 // ensure that layers were mounted from the first repo during push 230 assert.Assert(c, strings.Contains(out2, "Mounted from dockercli/busybox")) 231 232 digest2 := reference.DigestRegexp.FindString(out2) 233 assert.Assert(c, len(digest2) > 0, "no digest found for pushed manifest") 234 assert.Equal(c, digest1, digest2) 235 236 // ensure that pushing again produces the same digest 237 out3, _, err := dockerCmdWithError("push", destRepoName) 238 assert.NilError(c, err, "pushing the image to the private registry has failed: %s", out3) 239 240 digest3 := reference.DigestRegexp.FindString(out3) 241 assert.Assert(c, len(digest3) > 0, "no digest found for pushed manifest") 242 assert.Equal(c, digest3, digest2) 243 244 // ensure that we can pull and run the cross-repo-pushed repository 245 dockerCmd(c, "rmi", destRepoName) 246 dockerCmd(c, "pull", destRepoName) 247 out4, _ := dockerCmd(c, "run", destRepoName, "echo", "-n", "hello world") 248 assert.Equal(c, out4, "hello world") 249 } 250 251 func (s *DockerSchema1RegistrySuite) TestCrossRepositoryLayerPushNotSupported(c *testing.T) { 252 sourceRepoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 253 // tag the image to upload it to the private registry 254 dockerCmd(c, "tag", "busybox", sourceRepoName) 255 // push the image to the registry 256 out1, _, err := dockerCmdWithError("push", sourceRepoName) 257 assert.NilError(c, err, fmt.Sprintf("pushing the image to the private registry has failed: %s", out1)) 258 // ensure that none of the layers were mounted from another repository during push 259 assert.Assert(c, !strings.Contains(out1, "Mounted from")) 260 261 digest1 := reference.DigestRegexp.FindString(out1) 262 assert.Assert(c, len(digest1) > 0, "no digest found for pushed manifest") 263 264 destRepoName := fmt.Sprintf("%v/dockercli/crossrepopush", privateRegistryURL) 265 // retag the image to upload the same layers to another repo in the same registry 266 dockerCmd(c, "tag", "busybox", destRepoName) 267 // push the image to the registry 268 out2, _, err := dockerCmdWithError("push", destRepoName) 269 assert.NilError(c, err, fmt.Sprintf("pushing the image to the private registry has failed: %s", out2)) 270 // schema1 registry should not support cross-repo layer mounts, so ensure that this does not happen 271 assert.Assert(c, !strings.Contains(out2, "Mounted from")) 272 273 digest2 := reference.DigestRegexp.FindString(out2) 274 assert.Assert(c, len(digest2) > 0, "no digest found for pushed manifest") 275 assert.Assert(c, digest1 != digest2) 276 277 // ensure that we can pull and run the second pushed repository 278 dockerCmd(c, "rmi", destRepoName) 279 dockerCmd(c, "pull", destRepoName) 280 out3, _ := dockerCmd(c, "run", destRepoName, "echo", "-n", "hello world") 281 assert.Assert(c, out3 == "hello world") 282 } 283 284 func (s *DockerRegistryAuthHtpasswdSuite) TestPushNoCredentialsNoRetry(c *testing.T) { 285 repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) 286 dockerCmd(c, "tag", "busybox", repoName) 287 out, _, err := dockerCmdWithError("push", repoName) 288 assert.ErrorContains(c, err, "", out) 289 assert.Assert(c, !strings.Contains(out, "Retrying")) 290 assert.Assert(c, strings.Contains(out, "no basic auth credentials")) 291 } 292 293 // This may be flaky but it's needed not to regress on unauthorized push, see #21054 294 func (s *DockerSuite) TestPushToCentralRegistryUnauthorized(c *testing.T) { 295 testRequires(c, Network) 296 repoName := "test/busybox" 297 dockerCmd(c, "tag", "busybox", repoName) 298 out, _, err := dockerCmdWithError("push", repoName) 299 assert.ErrorContains(c, err, "", out) 300 assert.Assert(c, !strings.Contains(out, "Retrying")) 301 } 302 303 func getTestTokenService(status int, body string, retries int) *httptest.Server { 304 var mu sync.Mutex 305 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 306 mu.Lock() 307 if retries > 0 { 308 w.WriteHeader(http.StatusServiceUnavailable) 309 w.Header().Set("Content-Type", "application/json") 310 w.Write([]byte(`{"errors":[{"code":"UNAVAILABLE","message":"cannot create token at this time"}]}`)) 311 retries-- 312 } else { 313 w.WriteHeader(status) 314 w.Header().Set("Content-Type", "application/json") 315 w.Write([]byte(body)) 316 } 317 mu.Unlock() 318 })) 319 } 320 321 func (s *DockerRegistryAuthTokenSuite) TestPushTokenServiceUnauthResponse(c *testing.T) { 322 ts := getTestTokenService(http.StatusUnauthorized, `{"errors": [{"Code":"UNAUTHORIZED", "message": "a message", "detail": null}]}`, 0) 323 defer ts.Close() 324 s.setupRegistryWithTokenService(c, ts.URL) 325 repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) 326 dockerCmd(c, "tag", "busybox", repoName) 327 out, _, err := dockerCmdWithError("push", repoName) 328 assert.ErrorContains(c, err, "", out) 329 assert.Assert(c, !strings.Contains(out, "Retrying")) 330 assert.Assert(c, strings.Contains(out, "unauthorized: a message")) 331 } 332 333 func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseUnauthorized(c *testing.T) { 334 ts := getTestTokenService(http.StatusUnauthorized, `{"error": "unauthorized"}`, 0) 335 defer ts.Close() 336 s.setupRegistryWithTokenService(c, ts.URL) 337 repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) 338 dockerCmd(c, "tag", "busybox", repoName) 339 out, _, err := dockerCmdWithError("push", repoName) 340 assert.ErrorContains(c, err, "", out) 341 assert.Assert(c, !strings.Contains(out, "Retrying")) 342 split := strings.Split(out, "\n") 343 assert.Equal(c, split[len(split)-2], "unauthorized: authentication required") 344 } 345 346 func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseError(c *testing.T) { 347 ts := getTestTokenService(http.StatusTooManyRequests, `{"errors": [{"code":"TOOMANYREQUESTS","message":"out of tokens"}]}`, 3) 348 defer ts.Close() 349 s.setupRegistryWithTokenService(c, ts.URL) 350 repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) 351 dockerCmd(c, "tag", "busybox", repoName) 352 out, _, err := dockerCmdWithError("push", repoName) 353 assert.ErrorContains(c, err, "", out) 354 // TODO: isolate test so that it can be guaranteed that the 503 will trigger xfer retries 355 //assert.Assert(c, strings.Contains(out, "Retrying")) 356 //assert.Assert(c, !strings.Contains(out, "Retrying in 15")) 357 split := strings.Split(out, "\n") 358 assert.Equal(c, split[len(split)-2], "toomanyrequests: out of tokens") 359 } 360 361 func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseUnparsable(c *testing.T) { 362 ts := getTestTokenService(http.StatusForbidden, `no way`, 0) 363 defer ts.Close() 364 s.setupRegistryWithTokenService(c, ts.URL) 365 repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) 366 dockerCmd(c, "tag", "busybox", repoName) 367 out, _, err := dockerCmdWithError("push", repoName) 368 assert.ErrorContains(c, err, "", out) 369 assert.Assert(c, !strings.Contains(out, "Retrying")) 370 split := strings.Split(out, "\n") 371 assert.Assert(c, strings.Contains(split[len(split)-2], "error parsing HTTP 403 response body: ")) 372 } 373 374 func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseNoToken(c *testing.T) { 375 ts := getTestTokenService(http.StatusOK, `{"something": "wrong"}`, 0) 376 defer ts.Close() 377 s.setupRegistryWithTokenService(c, ts.URL) 378 repoName := fmt.Sprintf("%s/busybox", privateRegistryURL) 379 dockerCmd(c, "tag", "busybox", repoName) 380 out, _, err := dockerCmdWithError("push", repoName) 381 assert.ErrorContains(c, err, "", out) 382 assert.Assert(c, !strings.Contains(out, "Retrying")) 383 split := strings.Split(out, "\n") 384 assert.Equal(c, split[len(split)-2], "authorization server did not include a token in the response") 385 }