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