build_test.go 15 KB

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