build_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. package build // import "github.com/docker/docker/integration/build"
  2. import (
  3. "archive/tar"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "io"
  8. "io/ioutil"
  9. "strings"
  10. "testing"
  11. "github.com/docker/docker/api/types"
  12. "github.com/docker/docker/api/types/filters"
  13. "github.com/docker/docker/api/types/versions"
  14. "github.com/docker/docker/errdefs"
  15. "github.com/docker/docker/internal/test/fakecontext"
  16. "github.com/docker/docker/pkg/jsonmessage"
  17. "gotest.tools/assert"
  18. is "gotest.tools/assert/cmp"
  19. "gotest.tools/skip"
  20. )
  21. func TestBuildWithRemoveAndForceRemove(t *testing.T) {
  22. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  23. defer setupTest(t)()
  24. cases := []struct {
  25. name string
  26. dockerfile string
  27. numberOfIntermediateContainers int
  28. rm bool
  29. forceRm bool
  30. }{
  31. {
  32. name: "successful build with no removal",
  33. dockerfile: `FROM busybox
  34. RUN exit 0
  35. RUN exit 0`,
  36. numberOfIntermediateContainers: 2,
  37. rm: false,
  38. forceRm: false,
  39. },
  40. {
  41. name: "successful build with remove",
  42. dockerfile: `FROM busybox
  43. RUN exit 0
  44. RUN exit 0`,
  45. numberOfIntermediateContainers: 0,
  46. rm: true,
  47. forceRm: false,
  48. },
  49. {
  50. name: "successful build with remove and force remove",
  51. dockerfile: `FROM busybox
  52. RUN exit 0
  53. RUN exit 0`,
  54. numberOfIntermediateContainers: 0,
  55. rm: true,
  56. forceRm: true,
  57. },
  58. {
  59. name: "failed build with no removal",
  60. dockerfile: `FROM busybox
  61. RUN exit 0
  62. RUN exit 1`,
  63. numberOfIntermediateContainers: 2,
  64. rm: false,
  65. forceRm: false,
  66. },
  67. {
  68. name: "failed build with remove",
  69. dockerfile: `FROM busybox
  70. RUN exit 0
  71. RUN exit 1`,
  72. numberOfIntermediateContainers: 1,
  73. rm: true,
  74. forceRm: false,
  75. },
  76. {
  77. name: "failed build with remove and force remove",
  78. dockerfile: `FROM busybox
  79. RUN exit 0
  80. RUN exit 1`,
  81. numberOfIntermediateContainers: 0,
  82. rm: true,
  83. forceRm: true,
  84. },
  85. }
  86. client := testEnv.APIClient()
  87. ctx := context.Background()
  88. for _, c := range cases {
  89. t.Run(c.name, func(t *testing.T) {
  90. t.Parallel()
  91. dockerfile := []byte(c.dockerfile)
  92. buff := bytes.NewBuffer(nil)
  93. tw := tar.NewWriter(buff)
  94. assert.NilError(t, tw.WriteHeader(&tar.Header{
  95. Name: "Dockerfile",
  96. Size: int64(len(dockerfile)),
  97. }))
  98. _, err := tw.Write(dockerfile)
  99. assert.NilError(t, err)
  100. assert.NilError(t, tw.Close())
  101. resp, err := client.ImageBuild(ctx, buff, types.ImageBuildOptions{Remove: c.rm, ForceRemove: c.forceRm, NoCache: true})
  102. assert.NilError(t, err)
  103. defer resp.Body.Close()
  104. filter, err := buildContainerIdsFilter(resp.Body)
  105. assert.NilError(t, err)
  106. remainingContainers, err := client.ContainerList(ctx, types.ContainerListOptions{Filters: filter, All: true})
  107. assert.NilError(t, err)
  108. assert.Equal(t, c.numberOfIntermediateContainers, len(remainingContainers), "Expected %v remaining intermediate containers, got %v", c.numberOfIntermediateContainers, len(remainingContainers))
  109. })
  110. }
  111. }
  112. func buildContainerIdsFilter(buildOutput io.Reader) (filters.Args, error) {
  113. const intermediateContainerPrefix = " ---> Running in "
  114. filter := filters.NewArgs()
  115. dec := json.NewDecoder(buildOutput)
  116. for {
  117. m := jsonmessage.JSONMessage{}
  118. err := dec.Decode(&m)
  119. if err == io.EOF {
  120. return filter, nil
  121. }
  122. if err != nil {
  123. return filter, err
  124. }
  125. if ix := strings.Index(m.Stream, intermediateContainerPrefix); ix != -1 {
  126. filter.Add("id", strings.TrimSpace(m.Stream[ix+len(intermediateContainerPrefix):]))
  127. }
  128. }
  129. }
  130. func TestBuildMultiStageParentConfig(t *testing.T) {
  131. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.35"), "broken in earlier versions")
  132. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  133. dockerfile := `
  134. FROM busybox AS stage0
  135. ENV WHO=parent
  136. WORKDIR /foo
  137. FROM stage0
  138. ENV WHO=sibling1
  139. WORKDIR sub1
  140. FROM stage0
  141. WORKDIR sub2
  142. `
  143. ctx := context.Background()
  144. source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
  145. defer source.Close()
  146. apiclient := testEnv.APIClient()
  147. resp, err := apiclient.ImageBuild(ctx,
  148. source.AsTarReader(t),
  149. types.ImageBuildOptions{
  150. Remove: true,
  151. ForceRemove: true,
  152. Tags: []string{"build1"},
  153. })
  154. assert.NilError(t, err)
  155. _, err = io.Copy(ioutil.Discard, resp.Body)
  156. resp.Body.Close()
  157. assert.NilError(t, err)
  158. image, _, err := apiclient.ImageInspectWithRaw(ctx, "build1")
  159. assert.NilError(t, err)
  160. expected := "/foo/sub2"
  161. if testEnv.DaemonInfo.OSType == "windows" {
  162. expected = `C:\foo\sub2`
  163. }
  164. assert.Check(t, is.Equal(expected, image.Config.WorkingDir))
  165. assert.Check(t, is.Contains(image.Config.Env, "WHO=parent"))
  166. }
  167. // Test cases in #36996
  168. func TestBuildLabelWithTargets(t *testing.T) {
  169. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.38"), "test added after 1.38")
  170. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  171. bldName := "build-a"
  172. testLabels := map[string]string{
  173. "foo": "bar",
  174. "dead": "beef",
  175. }
  176. dockerfile := `
  177. FROM busybox AS target-a
  178. CMD ["/dev"]
  179. LABEL label-a=inline-a
  180. FROM busybox AS target-b
  181. CMD ["/dist"]
  182. LABEL label-b=inline-b
  183. `
  184. ctx := context.Background()
  185. source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
  186. defer source.Close()
  187. apiclient := testEnv.APIClient()
  188. // For `target-a` build
  189. resp, err := apiclient.ImageBuild(ctx,
  190. source.AsTarReader(t),
  191. types.ImageBuildOptions{
  192. Remove: true,
  193. ForceRemove: true,
  194. Tags: []string{bldName},
  195. Labels: testLabels,
  196. Target: "target-a",
  197. })
  198. assert.NilError(t, err)
  199. _, err = io.Copy(ioutil.Discard, resp.Body)
  200. resp.Body.Close()
  201. assert.NilError(t, err)
  202. image, _, err := apiclient.ImageInspectWithRaw(ctx, bldName)
  203. assert.NilError(t, err)
  204. testLabels["label-a"] = "inline-a"
  205. for k, v := range testLabels {
  206. x, ok := image.Config.Labels[k]
  207. assert.Assert(t, ok)
  208. assert.Assert(t, x == v)
  209. }
  210. // For `target-b` build
  211. bldName = "build-b"
  212. delete(testLabels, "label-a")
  213. resp, err = apiclient.ImageBuild(ctx,
  214. source.AsTarReader(t),
  215. types.ImageBuildOptions{
  216. Remove: true,
  217. ForceRemove: true,
  218. Tags: []string{bldName},
  219. Labels: testLabels,
  220. Target: "target-b",
  221. })
  222. assert.NilError(t, err)
  223. _, err = io.Copy(ioutil.Discard, resp.Body)
  224. resp.Body.Close()
  225. assert.NilError(t, err)
  226. image, _, err = apiclient.ImageInspectWithRaw(ctx, bldName)
  227. assert.NilError(t, err)
  228. testLabels["label-b"] = "inline-b"
  229. for k, v := range testLabels {
  230. x, ok := image.Config.Labels[k]
  231. assert.Assert(t, ok)
  232. assert.Assert(t, x == v)
  233. }
  234. }
  235. func TestBuildWithEmptyLayers(t *testing.T) {
  236. dockerfile := `
  237. FROM busybox
  238. COPY 1/ /target/
  239. COPY 2/ /target/
  240. COPY 3/ /target/
  241. `
  242. ctx := context.Background()
  243. source := fakecontext.New(t, "",
  244. fakecontext.WithDockerfile(dockerfile),
  245. fakecontext.WithFile("1/a", "asdf"),
  246. fakecontext.WithFile("2/a", "asdf"),
  247. fakecontext.WithFile("3/a", "asdf"))
  248. defer source.Close()
  249. apiclient := testEnv.APIClient()
  250. resp, err := apiclient.ImageBuild(ctx,
  251. source.AsTarReader(t),
  252. types.ImageBuildOptions{
  253. Remove: true,
  254. ForceRemove: true,
  255. })
  256. assert.NilError(t, err)
  257. _, err = io.Copy(ioutil.Discard, resp.Body)
  258. resp.Body.Close()
  259. assert.NilError(t, err)
  260. }
  261. // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to
  262. // multiple subsequent stages
  263. // #35652
  264. func TestBuildMultiStageOnBuild(t *testing.T) {
  265. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.33"), "broken in earlier versions")
  266. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  267. defer setupTest(t)()
  268. // test both metadata and layer based commands as they may be implemented differently
  269. dockerfile := `FROM busybox AS stage1
  270. ONBUILD RUN echo 'foo' >somefile
  271. ONBUILD ENV bar=baz
  272. FROM stage1
  273. # fails if ONBUILD RUN fails
  274. RUN cat somefile
  275. FROM stage1
  276. RUN cat somefile`
  277. ctx := context.Background()
  278. source := fakecontext.New(t, "",
  279. fakecontext.WithDockerfile(dockerfile))
  280. defer source.Close()
  281. apiclient := testEnv.APIClient()
  282. resp, err := apiclient.ImageBuild(ctx,
  283. source.AsTarReader(t),
  284. types.ImageBuildOptions{
  285. Remove: true,
  286. ForceRemove: true,
  287. })
  288. out := bytes.NewBuffer(nil)
  289. assert.NilError(t, err)
  290. _, err = io.Copy(out, resp.Body)
  291. resp.Body.Close()
  292. assert.NilError(t, err)
  293. assert.Check(t, is.Contains(out.String(), "Successfully built"))
  294. imageIDs, err := getImageIDsFromBuild(out.Bytes())
  295. assert.NilError(t, err)
  296. assert.Check(t, is.Equal(3, len(imageIDs)))
  297. image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2])
  298. assert.NilError(t, err)
  299. assert.Check(t, is.Contains(image.Config.Env, "bar=baz"))
  300. }
  301. // #35403 #36122
  302. func TestBuildUncleanTarFilenames(t *testing.T) {
  303. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
  304. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  305. ctx := context.TODO()
  306. defer setupTest(t)()
  307. dockerfile := `FROM scratch
  308. COPY foo /
  309. FROM scratch
  310. COPY bar /`
  311. buf := bytes.NewBuffer(nil)
  312. w := tar.NewWriter(buf)
  313. writeTarRecord(t, w, "Dockerfile", dockerfile)
  314. writeTarRecord(t, w, "../foo", "foocontents0")
  315. writeTarRecord(t, w, "/bar", "barcontents0")
  316. err := w.Close()
  317. assert.NilError(t, err)
  318. apiclient := testEnv.APIClient()
  319. resp, err := apiclient.ImageBuild(ctx,
  320. buf,
  321. types.ImageBuildOptions{
  322. Remove: true,
  323. ForceRemove: true,
  324. })
  325. out := bytes.NewBuffer(nil)
  326. assert.NilError(t, err)
  327. _, err = io.Copy(out, resp.Body)
  328. resp.Body.Close()
  329. assert.NilError(t, err)
  330. // repeat with changed data should not cause cache hits
  331. buf = bytes.NewBuffer(nil)
  332. w = tar.NewWriter(buf)
  333. writeTarRecord(t, w, "Dockerfile", dockerfile)
  334. writeTarRecord(t, w, "../foo", "foocontents1")
  335. writeTarRecord(t, w, "/bar", "barcontents1")
  336. err = w.Close()
  337. assert.NilError(t, err)
  338. resp, err = apiclient.ImageBuild(ctx,
  339. buf,
  340. types.ImageBuildOptions{
  341. Remove: true,
  342. ForceRemove: true,
  343. })
  344. out = bytes.NewBuffer(nil)
  345. assert.NilError(t, err)
  346. _, err = io.Copy(out, resp.Body)
  347. resp.Body.Close()
  348. assert.NilError(t, err)
  349. assert.Assert(t, !strings.Contains(out.String(), "Using cache"))
  350. }
  351. // docker/for-linux#135
  352. // #35641
  353. func TestBuildMultiStageLayerLeak(t *testing.T) {
  354. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  355. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
  356. ctx := context.TODO()
  357. defer setupTest(t)()
  358. // all commands need to match until COPY
  359. dockerfile := `FROM busybox
  360. WORKDIR /foo
  361. COPY foo .
  362. FROM busybox
  363. WORKDIR /foo
  364. COPY bar .
  365. RUN [ -f bar ]
  366. RUN [ ! -f foo ]
  367. `
  368. source := fakecontext.New(t, "",
  369. fakecontext.WithFile("foo", "0"),
  370. fakecontext.WithFile("bar", "1"),
  371. fakecontext.WithDockerfile(dockerfile))
  372. defer source.Close()
  373. apiclient := testEnv.APIClient()
  374. resp, err := apiclient.ImageBuild(ctx,
  375. source.AsTarReader(t),
  376. types.ImageBuildOptions{
  377. Remove: true,
  378. ForceRemove: true,
  379. })
  380. out := bytes.NewBuffer(nil)
  381. assert.NilError(t, err)
  382. _, err = io.Copy(out, resp.Body)
  383. resp.Body.Close()
  384. assert.NilError(t, err)
  385. assert.Check(t, is.Contains(out.String(), "Successfully built"))
  386. }
  387. // #37581
  388. func TestBuildWithHugeFile(t *testing.T) {
  389. skip.If(t, testEnv.OSType == "windows")
  390. ctx := context.TODO()
  391. defer setupTest(t)()
  392. dockerfile := `FROM busybox
  393. # create a sparse file with size over 8GB
  394. RUN for g in $(seq 0 8); do dd if=/dev/urandom of=rnd bs=1K count=1 seek=$((1024*1024*g)) status=none; done && \
  395. ls -la rnd && du -sk rnd`
  396. buf := bytes.NewBuffer(nil)
  397. w := tar.NewWriter(buf)
  398. writeTarRecord(t, w, "Dockerfile", dockerfile)
  399. err := w.Close()
  400. assert.NilError(t, err)
  401. apiclient := testEnv.APIClient()
  402. resp, err := apiclient.ImageBuild(ctx,
  403. buf,
  404. types.ImageBuildOptions{
  405. Remove: true,
  406. ForceRemove: true,
  407. })
  408. out := bytes.NewBuffer(nil)
  409. assert.NilError(t, err)
  410. _, err = io.Copy(out, resp.Body)
  411. resp.Body.Close()
  412. assert.NilError(t, err)
  413. assert.Check(t, is.Contains(out.String(), "Successfully built"))
  414. }
  415. func TestBuildWithEmptyDockerfile(t *testing.T) {
  416. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
  417. ctx := context.TODO()
  418. defer setupTest(t)()
  419. tests := []struct {
  420. name string
  421. dockerfile string
  422. expectedErr string
  423. }{
  424. {
  425. name: "empty-dockerfile",
  426. dockerfile: "",
  427. expectedErr: "cannot be empty",
  428. },
  429. {
  430. name: "empty-lines-dockerfile",
  431. dockerfile: `
  432. `,
  433. expectedErr: "file with no instructions",
  434. },
  435. {
  436. name: "comment-only-dockerfile",
  437. dockerfile: `# this is a comment`,
  438. expectedErr: "file with no instructions",
  439. },
  440. }
  441. apiclient := testEnv.APIClient()
  442. for _, tc := range tests {
  443. tc := tc
  444. t.Run(tc.name, func(t *testing.T) {
  445. t.Parallel()
  446. buf := bytes.NewBuffer(nil)
  447. w := tar.NewWriter(buf)
  448. writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
  449. err := w.Close()
  450. assert.NilError(t, err)
  451. _, err = apiclient.ImageBuild(ctx,
  452. buf,
  453. types.ImageBuildOptions{
  454. Remove: true,
  455. ForceRemove: true,
  456. })
  457. assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
  458. })
  459. }
  460. }
  461. func TestBuildPreserveOwnership(t *testing.T) {
  462. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  463. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
  464. ctx := context.Background()
  465. dockerfile, err := ioutil.ReadFile("testdata/Dockerfile.testBuildPreserveOwnership")
  466. assert.NilError(t, err)
  467. source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
  468. defer source.Close()
  469. apiclient := testEnv.APIClient()
  470. for _, target := range []string{"copy_from", "copy_from_chowned"} {
  471. t.Run(target, func(t *testing.T) {
  472. resp, err := apiclient.ImageBuild(
  473. ctx,
  474. source.AsTarReader(t),
  475. types.ImageBuildOptions{
  476. Remove: true,
  477. ForceRemove: true,
  478. Target: target,
  479. },
  480. )
  481. assert.NilError(t, err)
  482. out := bytes.NewBuffer(nil)
  483. assert.NilError(t, err)
  484. _, err = io.Copy(out, resp.Body)
  485. _ = resp.Body.Close()
  486. if err != nil {
  487. t.Log(out)
  488. }
  489. assert.NilError(t, err)
  490. })
  491. }
  492. }
  493. func TestBuildPlatformInvalid(t *testing.T) {
  494. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
  495. ctx := context.Background()
  496. defer setupTest(t)()
  497. dockerfile := `FROM busybox
  498. `
  499. buf := bytes.NewBuffer(nil)
  500. w := tar.NewWriter(buf)
  501. writeTarRecord(t, w, "Dockerfile", dockerfile)
  502. err := w.Close()
  503. assert.NilError(t, err)
  504. apiclient := testEnv.APIClient()
  505. _, err = apiclient.ImageBuild(ctx,
  506. buf,
  507. types.ImageBuildOptions{
  508. Remove: true,
  509. ForceRemove: true,
  510. Platform: "foobar",
  511. })
  512. assert.Assert(t, err != nil)
  513. assert.ErrorContains(t, err, "unknown operating system or architecture")
  514. assert.Assert(t, errdefs.IsInvalidParameter(err))
  515. }
  516. func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
  517. err := w.WriteHeader(&tar.Header{
  518. Name: fn,
  519. Mode: 0600,
  520. Size: int64(len(contents)),
  521. Typeflag: '0',
  522. })
  523. assert.NilError(t, err)
  524. _, err = w.Write([]byte(contents))
  525. assert.NilError(t, err)
  526. }
  527. type buildLine struct {
  528. Stream string
  529. Aux struct {
  530. ID string
  531. }
  532. }
  533. func getImageIDsFromBuild(output []byte) ([]string, error) {
  534. var ids []string
  535. for _, line := range bytes.Split(output, []byte("\n")) {
  536. if len(line) == 0 {
  537. continue
  538. }
  539. entry := buildLine{}
  540. if err := json.Unmarshal(line, &entry); err != nil {
  541. return nil, err
  542. }
  543. if entry.Aux.ID != "" {
  544. ids = append(ids, entry.Aux.ID)
  545. }
  546. }
  547. return ids, nil
  548. }