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