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