github.com/moby/docker@v26.1.3+incompatible/integration-cli/docker_api_build_test.go (about) 1 package main 2 3 import ( 4 "archive/tar" 5 "bytes" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "regexp" 11 "strings" 12 "testing" 13 14 "github.com/docker/docker/api/types/image" 15 "github.com/docker/docker/testutil" 16 "github.com/docker/docker/testutil/fakecontext" 17 "github.com/docker/docker/testutil/fakegit" 18 "github.com/docker/docker/testutil/fakestorage" 19 "github.com/docker/docker/testutil/request" 20 "gotest.tools/v3/assert" 21 is "gotest.tools/v3/assert/cmp" 22 ) 23 24 func (s *DockerAPISuite) TestBuildAPIDockerFileRemote(c *testing.T) { 25 testRequires(c, NotUserNamespace) 26 ctx := testutil.GetContext(c) 27 28 // -xdev is required because sysfs can cause EPERM 29 testD := `FROM busybox 30 RUN find / -xdev -name ba* 31 RUN find /tmp/` 32 server := fakestorage.New(c, "", fakecontext.WithFiles(map[string]string{"testD": testD})) 33 defer server.Close() 34 35 res, body, err := request.Post(ctx, "/build?dockerfile=baz&remote="+server.URL()+"/testD", request.JSON) 36 assert.NilError(c, err) 37 assert.Equal(c, res.StatusCode, http.StatusOK) 38 39 buf, err := request.ReadBody(body) 40 assert.NilError(c, err) 41 42 // Make sure Dockerfile exists. 43 // Make sure 'baz' doesn't exist ANYWHERE despite being mentioned in the URL 44 out := string(buf) 45 assert.Assert(c, is.Contains(out, "RUN find /tmp")) 46 assert.Assert(c, !strings.Contains(out, "baz")) 47 } 48 49 func (s *DockerAPISuite) TestBuildAPIRemoteTarballContext(c *testing.T) { 50 ctx := testutil.GetContext(c) 51 52 buffer := new(bytes.Buffer) 53 tw := tar.NewWriter(buffer) 54 defer tw.Close() 55 56 dockerfile := []byte("FROM busybox") 57 err := tw.WriteHeader(&tar.Header{ 58 Name: "Dockerfile", 59 Size: int64(len(dockerfile)), 60 }) 61 assert.NilError(c, err, "failed to write tar file header") 62 63 _, err = tw.Write(dockerfile) 64 assert.NilError(c, err, "failed to write tar file content") 65 assert.NilError(c, tw.Close(), "failed to close tar archive") 66 67 server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{ 68 "testT.tar": buffer, 69 })) 70 defer server.Close() 71 72 res, b, err := request.Post(ctx, "/build?remote="+server.URL()+"/testT.tar", request.ContentType("application/tar")) 73 assert.NilError(c, err) 74 assert.Equal(c, res.StatusCode, http.StatusOK) 75 b.Close() 76 } 77 78 func (s *DockerAPISuite) TestBuildAPIRemoteTarballContextWithCustomDockerfile(c *testing.T) { 79 buffer := new(bytes.Buffer) 80 tw := tar.NewWriter(buffer) 81 defer tw.Close() 82 83 dockerfile := []byte(`FROM busybox 84 RUN echo 'wrong'`) 85 err := tw.WriteHeader(&tar.Header{ 86 Name: "Dockerfile", 87 Size: int64(len(dockerfile)), 88 }) 89 // failed to write tar file header 90 assert.NilError(c, err) 91 92 _, err = tw.Write(dockerfile) 93 // failed to write tar file content 94 assert.NilError(c, err) 95 96 custom := []byte(`FROM busybox 97 RUN echo 'right' 98 `) 99 err = tw.WriteHeader(&tar.Header{ 100 Name: "custom", 101 Size: int64(len(custom)), 102 }) 103 104 // failed to write tar file header 105 assert.NilError(c, err) 106 107 _, err = tw.Write(custom) 108 // failed to write tar file content 109 assert.NilError(c, err) 110 111 // failed to close tar archive 112 assert.NilError(c, tw.Close()) 113 114 server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{ 115 "testT.tar": buffer, 116 })) 117 defer server.Close() 118 119 ctx := testutil.GetContext(c) 120 url := "/build?dockerfile=custom&remote=" + server.URL() + "/testT.tar" 121 res, body, err := request.Post(ctx, url, request.ContentType("application/tar")) 122 assert.NilError(c, err) 123 assert.Equal(c, res.StatusCode, http.StatusOK) 124 125 defer body.Close() 126 content, err := request.ReadBody(body) 127 assert.NilError(c, err) 128 129 // Build used the wrong dockerfile. 130 assert.Assert(c, !strings.Contains(string(content), "wrong")) 131 } 132 133 func (s *DockerAPISuite) TestBuildAPILowerDockerfile(c *testing.T) { 134 git := fakegit.New(c, "repo", map[string]string{ 135 "dockerfile": `FROM busybox 136 RUN echo from dockerfile`, 137 }, false) 138 defer git.Close() 139 140 ctx := testutil.GetContext(c) 141 res, body, err := request.Post(ctx, "/build?remote="+git.RepoURL, request.JSON) 142 assert.NilError(c, err) 143 assert.Equal(c, res.StatusCode, http.StatusOK) 144 145 buf, err := request.ReadBody(body) 146 assert.NilError(c, err) 147 148 out := string(buf) 149 assert.Assert(c, is.Contains(out, "from dockerfile")) 150 } 151 152 func (s *DockerAPISuite) TestBuildAPIBuildGitWithF(c *testing.T) { 153 git := fakegit.New(c, "repo", map[string]string{ 154 "baz": `FROM busybox 155 RUN echo from baz`, 156 "Dockerfile": `FROM busybox 157 RUN echo from Dockerfile`, 158 }, false) 159 defer git.Close() 160 161 ctx := testutil.GetContext(c) 162 // Make sure it tries to 'dockerfile' query param value 163 res, body, err := request.Post(ctx, "/build?dockerfile=baz&remote="+git.RepoURL, request.JSON) 164 assert.NilError(c, err) 165 assert.Equal(c, res.StatusCode, http.StatusOK) 166 167 buf, err := request.ReadBody(body) 168 assert.NilError(c, err) 169 170 out := string(buf) 171 assert.Assert(c, is.Contains(out, "from baz")) 172 } 173 174 func (s *DockerAPISuite) TestBuildAPIDoubleDockerfile(c *testing.T) { 175 testRequires(c, UnixCli) // dockerfile overwrites Dockerfile on Windows 176 git := fakegit.New(c, "repo", map[string]string{ 177 "Dockerfile": `FROM busybox 178 RUN echo from Dockerfile`, 179 "dockerfile": `FROM busybox 180 RUN echo from dockerfile`, 181 }, false) 182 defer git.Close() 183 184 ctx := testutil.GetContext(c) 185 186 // Make sure it tries to 'dockerfile' query param value 187 res, body, err := request.Post(ctx, "/build?remote="+git.RepoURL, request.JSON) 188 assert.NilError(c, err) 189 assert.Equal(c, res.StatusCode, http.StatusOK) 190 191 buf, err := request.ReadBody(body) 192 assert.NilError(c, err) 193 194 out := string(buf) 195 assert.Assert(c, is.Contains(out, "from Dockerfile")) 196 } 197 198 func (s *DockerAPISuite) TestBuildAPIUnnormalizedTarPaths(c *testing.T) { 199 // Make sure that build context tars with entries of the form 200 // x/./y don't cause caching false positives. 201 202 buildFromTarContext := func(fileContents []byte) string { 203 buffer := new(bytes.Buffer) 204 tw := tar.NewWriter(buffer) 205 defer tw.Close() 206 207 dockerfile := []byte(`FROM busybox 208 COPY dir /dir/`) 209 err := tw.WriteHeader(&tar.Header{ 210 Name: "Dockerfile", 211 Size: int64(len(dockerfile)), 212 }) 213 assert.NilError(c, err, "failed to write tar file header") 214 215 _, err = tw.Write(dockerfile) 216 assert.NilError(c, err, "failed to write Dockerfile in tar file content") 217 218 err = tw.WriteHeader(&tar.Header{ 219 Name: "dir/./file", 220 Size: int64(len(fileContents)), 221 }) 222 assert.NilError(c, err, "failed to write tar file header") 223 224 _, err = tw.Write(fileContents) 225 assert.NilError(c, err, "failed to write file contents in tar file content") 226 227 assert.NilError(c, tw.Close(), "failed to close tar archive") 228 229 ctx := testutil.GetContext(c) 230 231 res, body, err := request.Post(ctx, "/build", request.RawContent(io.NopCloser(buffer)), request.ContentType("application/x-tar")) 232 assert.NilError(c, err) 233 assert.Equal(c, res.StatusCode, http.StatusOK) 234 235 out, err := request.ReadBody(body) 236 assert.NilError(c, err) 237 lines := strings.Split(string(out), "\n") 238 assert.Assert(c, len(lines) > 1) 239 matched, err := regexp.MatchString(".*Successfully built [0-9a-f]{12}.*", lines[len(lines)-2]) 240 assert.NilError(c, err) 241 assert.Assert(c, matched) 242 243 re := regexp.MustCompile("Successfully built ([0-9a-f]{12})") 244 matches := re.FindStringSubmatch(lines[len(lines)-2]) 245 return matches[1] 246 } 247 248 imageA := buildFromTarContext([]byte("abc")) 249 imageB := buildFromTarContext([]byte("def")) 250 251 assert.Assert(c, imageA != imageB) 252 } 253 254 func (s *DockerAPISuite) TestBuildOnBuildWithCopy(c *testing.T) { 255 dockerfile := ` 256 FROM ` + minimalBaseImage() + ` as onbuildbase 257 ONBUILD COPY file /file 258 259 FROM onbuildbase 260 ` 261 bCtx := fakecontext.New(c, "", 262 fakecontext.WithDockerfile(dockerfile), 263 fakecontext.WithFile("file", "some content"), 264 ) 265 defer bCtx.Close() 266 267 ctx := testutil.GetContext(c) 268 res, body, err := request.Post( 269 ctx, 270 "/build", 271 request.RawContent(bCtx.AsTarReader(c)), 272 request.ContentType("application/x-tar")) 273 assert.NilError(c, err) 274 assert.Equal(c, res.StatusCode, http.StatusOK) 275 276 out, err := request.ReadBody(body) 277 assert.NilError(c, err) 278 assert.Assert(c, is.Contains(string(out), "Successfully built")) 279 } 280 281 func (s *DockerAPISuite) TestBuildOnBuildCache(c *testing.T) { 282 build := func(dockerfile string) []byte { 283 bCtx := fakecontext.New(c, "", 284 fakecontext.WithDockerfile(dockerfile), 285 ) 286 defer bCtx.Close() 287 288 ctx := testutil.GetContext(c) 289 res, body, err := request.Post( 290 ctx, 291 "/build", 292 request.RawContent(bCtx.AsTarReader(c)), 293 request.ContentType("application/x-tar")) 294 assert.NilError(c, err) 295 assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode)) 296 297 out, err := request.ReadBody(body) 298 assert.NilError(c, err) 299 assert.Assert(c, is.Contains(string(out), "Successfully built")) 300 return out 301 } 302 303 dockerfile := ` 304 FROM ` + minimalBaseImage() + ` as onbuildbase 305 ENV something=bar 306 ONBUILD ENV foo=bar 307 ` 308 build(dockerfile) 309 310 dockerfile += "FROM onbuildbase" 311 out := build(dockerfile) 312 313 imageIDs := getImageIDsFromBuild(c, out) 314 assert.Assert(c, is.Len(imageIDs, 2)) 315 parentID, childID := imageIDs[0], imageIDs[1] 316 317 client := testEnv.APIClient() 318 ctx := testutil.GetContext(c) 319 320 // check parentID is correct 321 // Parent is graphdriver-only 322 if !testEnv.UsingSnapshotter() { 323 image, _, err := client.ImageInspectWithRaw(ctx, childID) 324 assert.NilError(c, err) 325 326 assert.Check(c, is.Equal(parentID, image.Parent)) 327 } 328 } 329 330 func (s *DockerRegistrySuite) TestBuildCopyFromForcePull(c *testing.T) { 331 client := testEnv.APIClient() 332 333 repoName := fmt.Sprintf("%v/dockercli/busybox", privateRegistryURL) 334 // tag the image to upload it to the private registry 335 ctx := testutil.GetContext(c) 336 err := client.ImageTag(ctx, "busybox", repoName) 337 assert.Check(c, err) 338 // push the image to the registry 339 rc, err := client.ImagePush(ctx, repoName, image.PushOptions{RegistryAuth: "{}"}) 340 assert.Check(c, err) 341 _, err = io.Copy(io.Discard, rc) 342 assert.Check(c, err) 343 344 dockerfile := fmt.Sprintf(` 345 FROM %s AS foo 346 RUN touch abc 347 FROM %s 348 COPY --from=foo /abc / 349 `, repoName, repoName) 350 351 bCtx := fakecontext.New(c, "", 352 fakecontext.WithDockerfile(dockerfile), 353 ) 354 defer bCtx.Close() 355 356 res, body, err := request.Post( 357 ctx, 358 "/build?pull=1", 359 request.RawContent(bCtx.AsTarReader(c)), 360 request.ContentType("application/x-tar")) 361 assert.NilError(c, err) 362 assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode)) 363 364 out, err := request.ReadBody(body) 365 assert.NilError(c, err) 366 assert.Check(c, is.Contains(string(out), "Successfully built")) 367 } 368 369 func (s *DockerAPISuite) TestBuildAddRemoteNoDecompress(c *testing.T) { 370 buffer := new(bytes.Buffer) 371 tw := tar.NewWriter(buffer) 372 dt := []byte("contents") 373 err := tw.WriteHeader(&tar.Header{ 374 Name: "foo", 375 Size: int64(len(dt)), 376 Mode: 0o600, 377 Typeflag: tar.TypeReg, 378 }) 379 assert.NilError(c, err) 380 _, err = tw.Write(dt) 381 assert.NilError(c, err) 382 err = tw.Close() 383 assert.NilError(c, err) 384 385 server := fakestorage.New(c, "", fakecontext.WithBinaryFiles(map[string]*bytes.Buffer{ 386 "test.tar": buffer, 387 })) 388 defer server.Close() 389 390 dockerfile := fmt.Sprintf(` 391 FROM busybox 392 ADD %s/test.tar / 393 RUN [ -f test.tar ] 394 `, server.URL()) 395 396 bCtx := fakecontext.New(c, "", 397 fakecontext.WithDockerfile(dockerfile), 398 ) 399 defer bCtx.Close() 400 401 ctx := testutil.GetContext(c) 402 res, body, err := request.Post( 403 ctx, 404 "/build", 405 request.RawContent(bCtx.AsTarReader(c)), 406 request.ContentType("application/x-tar")) 407 assert.NilError(c, err) 408 assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode)) 409 410 out, err := request.ReadBody(body) 411 assert.NilError(c, err) 412 assert.Check(c, is.Contains(string(out), "Successfully built")) 413 } 414 415 func (s *DockerAPISuite) TestBuildChownOnCopy(c *testing.T) { 416 testRequires(c, DaemonIsLinux) 417 dockerfile := `FROM busybox 418 RUN echo 'test1:x:1001:1001::/bin:/bin/false' >> /etc/passwd 419 RUN echo 'test1:x:1001:' >> /etc/group 420 RUN echo 'test2:x:1002:' >> /etc/group 421 COPY --chown=test1:1002 . /new_dir 422 RUN ls -l / 423 RUN [ $(ls -l / | grep new_dir | awk '{print $3":"$4}') = 'test1:test2' ] 424 RUN [ $(ls -nl / | grep new_dir | awk '{print $3":"$4}') = '1001:1002' ] 425 ` 426 bCtx := fakecontext.New(c, "", 427 fakecontext.WithDockerfile(dockerfile), 428 fakecontext.WithFile("test_file1", "some test content"), 429 ) 430 defer bCtx.Close() 431 432 ctx := testutil.GetContext(c) 433 res, body, err := request.Post( 434 ctx, 435 "/build", 436 request.RawContent(bCtx.AsTarReader(c)), 437 request.ContentType("application/x-tar")) 438 assert.NilError(c, err) 439 assert.Equal(c, res.StatusCode, http.StatusOK) 440 441 out, err := request.ReadBody(body) 442 assert.NilError(c, err) 443 assert.Check(c, is.Contains(string(out), "Successfully built")) 444 } 445 446 func (s *DockerAPISuite) TestBuildCopyCacheOnFileChange(c *testing.T) { 447 dockerfile := `FROM busybox 448 COPY file /file` 449 450 ctx1 := fakecontext.New(c, "", 451 fakecontext.WithDockerfile(dockerfile), 452 fakecontext.WithFile("file", "foo")) 453 ctx2 := fakecontext.New(c, "", 454 fakecontext.WithDockerfile(dockerfile), 455 fakecontext.WithFile("file", "bar")) 456 457 ctx := testutil.GetContext(c) 458 build := func(bCtx *fakecontext.Fake) string { 459 res, body, err := request.Post(ctx, "/build", 460 request.RawContent(bCtx.AsTarReader(c)), 461 request.ContentType("application/x-tar")) 462 463 assert.NilError(c, err) 464 assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode)) 465 466 out, err := request.ReadBody(body) 467 assert.NilError(c, err) 468 assert.Assert(c, is.Contains(string(out), "Successfully built")) 469 470 ids := getImageIDsFromBuild(c, out) 471 assert.Assert(c, is.Len(ids, 1)) 472 return ids[len(ids)-1] 473 } 474 475 id1 := build(ctx1) 476 id2 := build(ctx1) 477 id3 := build(ctx2) 478 479 if id1 != id2 { 480 c.Fatal("didn't use the cache") 481 } 482 if id1 == id3 { 483 c.Fatal("COPY With different source file should not share same cache") 484 } 485 } 486 487 func (s *DockerAPISuite) TestBuildAddCacheOnFileChange(c *testing.T) { 488 dockerfile := `FROM busybox 489 ADD file /file` 490 491 ctx1 := fakecontext.New(c, "", 492 fakecontext.WithDockerfile(dockerfile), 493 fakecontext.WithFile("file", "foo")) 494 ctx2 := fakecontext.New(c, "", 495 fakecontext.WithDockerfile(dockerfile), 496 fakecontext.WithFile("file", "bar")) 497 498 ctx := testutil.GetContext(c) 499 build := func(bCtx *fakecontext.Fake) string { 500 res, body, err := request.Post(ctx, "/build", 501 request.RawContent(bCtx.AsTarReader(c)), 502 request.ContentType("application/x-tar")) 503 504 assert.NilError(c, err) 505 assert.Check(c, is.DeepEqual(http.StatusOK, res.StatusCode)) 506 507 out, err := request.ReadBody(body) 508 assert.NilError(c, err) 509 assert.Assert(c, is.Contains(string(out), "Successfully built")) 510 511 ids := getImageIDsFromBuild(c, out) 512 assert.Assert(c, is.Len(ids, 1)) 513 return ids[len(ids)-1] 514 } 515 516 id1 := build(ctx1) 517 id2 := build(ctx1) 518 id3 := build(ctx2) 519 520 if id1 != id2 { 521 c.Fatal("didn't use the cache") 522 } 523 if id1 == id3 { 524 c.Fatal("COPY With different source file should not share same cache") 525 } 526 } 527 528 func (s *DockerAPISuite) TestBuildScratchCopy(c *testing.T) { 529 testRequires(c, DaemonIsLinux) 530 dockerfile := `FROM scratch 531 ADD Dockerfile / 532 ENV foo bar` 533 bCtx := fakecontext.New(c, "", 534 fakecontext.WithDockerfile(dockerfile), 535 ) 536 defer bCtx.Close() 537 538 ctx := testutil.GetContext(c) 539 res, body, err := request.Post( 540 ctx, 541 "/build", 542 request.RawContent(bCtx.AsTarReader(c)), 543 request.ContentType("application/x-tar")) 544 assert.NilError(c, err) 545 assert.Equal(c, res.StatusCode, http.StatusOK) 546 547 out, err := request.ReadBody(body) 548 assert.NilError(c, err) 549 assert.Check(c, is.Contains(string(out), "Successfully built")) 550 } 551 552 type buildLine struct { 553 Stream string 554 Aux struct { 555 ID string 556 } 557 } 558 559 func getImageIDsFromBuild(c *testing.T, output []byte) []string { 560 var ids []string 561 for _, line := range bytes.Split(output, []byte("\n")) { 562 if len(line) == 0 { 563 continue 564 } 565 entry := buildLine{} 566 assert.NilError(c, json.Unmarshal(line, &entry)) 567 if entry.Aux.ID != "" { 568 ids = append(ids, entry.Aux.ID) 569 } 570 } 571 return ids 572 }