build_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  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. // TestBuildMultiStageCopy verifies that copying between stages works correctly.
  131. //
  132. // Regression test for docker/for-win#4349, ENGCORE-935, where creating the target
  133. // directory failed on Windows, because `os.MkdirAll()` was called with a volume
  134. // GUID path (\\?\Volume{dae8d3ac-b9a1-11e9-88eb-e8554b2ba1db}\newdir\hello}),
  135. // which currently isn't supported by Golang.
  136. func TestBuildMultiStageCopy(t *testing.T) {
  137. ctx := context.Background()
  138. dockerfile, err := ioutil.ReadFile("testdata/Dockerfile." + t.Name())
  139. assert.NilError(t, err)
  140. source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
  141. defer source.Close()
  142. apiclient := testEnv.APIClient()
  143. for _, target := range []string{"copy_to_root", "copy_to_newdir", "copy_to_newdir_nested", "copy_to_existingdir", "copy_to_newsubdir"} {
  144. t.Run(target, func(t *testing.T) {
  145. imgName := strings.ToLower(t.Name())
  146. resp, err := apiclient.ImageBuild(
  147. ctx,
  148. source.AsTarReader(t),
  149. types.ImageBuildOptions{
  150. Remove: true,
  151. ForceRemove: true,
  152. Target: target,
  153. Tags: []string{imgName},
  154. },
  155. )
  156. assert.NilError(t, err)
  157. out := bytes.NewBuffer(nil)
  158. _, err = io.Copy(out, resp.Body)
  159. _ = resp.Body.Close()
  160. if err != nil {
  161. t.Log(out)
  162. }
  163. assert.NilError(t, err)
  164. // verify the image was successfully built
  165. _, _, err = apiclient.ImageInspectWithRaw(ctx, imgName)
  166. if err != nil {
  167. t.Log(out)
  168. }
  169. assert.NilError(t, err)
  170. })
  171. }
  172. }
  173. func TestBuildMultiStageParentConfig(t *testing.T) {
  174. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.35"), "broken in earlier versions")
  175. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  176. dockerfile := `
  177. FROM busybox AS stage0
  178. ENV WHO=parent
  179. WORKDIR /foo
  180. FROM stage0
  181. ENV WHO=sibling1
  182. WORKDIR sub1
  183. FROM stage0
  184. WORKDIR sub2
  185. `
  186. ctx := context.Background()
  187. source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
  188. defer source.Close()
  189. apiclient := testEnv.APIClient()
  190. resp, err := apiclient.ImageBuild(ctx,
  191. source.AsTarReader(t),
  192. types.ImageBuildOptions{
  193. Remove: true,
  194. ForceRemove: true,
  195. Tags: []string{"build1"},
  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, "build1")
  202. assert.NilError(t, err)
  203. expected := "/foo/sub2"
  204. if testEnv.DaemonInfo.OSType == "windows" {
  205. expected = `C:\foo\sub2`
  206. }
  207. assert.Check(t, is.Equal(expected, image.Config.WorkingDir))
  208. assert.Check(t, is.Contains(image.Config.Env, "WHO=parent"))
  209. }
  210. // Test cases in #36996
  211. func TestBuildLabelWithTargets(t *testing.T) {
  212. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.38"), "test added after 1.38")
  213. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  214. bldName := "build-a"
  215. testLabels := map[string]string{
  216. "foo": "bar",
  217. "dead": "beef",
  218. }
  219. dockerfile := `
  220. FROM busybox AS target-a
  221. CMD ["/dev"]
  222. LABEL label-a=inline-a
  223. FROM busybox AS target-b
  224. CMD ["/dist"]
  225. LABEL label-b=inline-b
  226. `
  227. ctx := context.Background()
  228. source := fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))
  229. defer source.Close()
  230. apiclient := testEnv.APIClient()
  231. // For `target-a` build
  232. resp, err := apiclient.ImageBuild(ctx,
  233. source.AsTarReader(t),
  234. types.ImageBuildOptions{
  235. Remove: true,
  236. ForceRemove: true,
  237. Tags: []string{bldName},
  238. Labels: testLabels,
  239. Target: "target-a",
  240. })
  241. assert.NilError(t, err)
  242. _, err = io.Copy(ioutil.Discard, resp.Body)
  243. resp.Body.Close()
  244. assert.NilError(t, err)
  245. image, _, err := apiclient.ImageInspectWithRaw(ctx, bldName)
  246. assert.NilError(t, err)
  247. testLabels["label-a"] = "inline-a"
  248. for k, v := range testLabels {
  249. x, ok := image.Config.Labels[k]
  250. assert.Assert(t, ok)
  251. assert.Assert(t, x == v)
  252. }
  253. // For `target-b` build
  254. bldName = "build-b"
  255. delete(testLabels, "label-a")
  256. resp, err = apiclient.ImageBuild(ctx,
  257. source.AsTarReader(t),
  258. types.ImageBuildOptions{
  259. Remove: true,
  260. ForceRemove: true,
  261. Tags: []string{bldName},
  262. Labels: testLabels,
  263. Target: "target-b",
  264. })
  265. assert.NilError(t, err)
  266. _, err = io.Copy(ioutil.Discard, resp.Body)
  267. resp.Body.Close()
  268. assert.NilError(t, err)
  269. image, _, err = apiclient.ImageInspectWithRaw(ctx, bldName)
  270. assert.NilError(t, err)
  271. testLabels["label-b"] = "inline-b"
  272. for k, v := range testLabels {
  273. x, ok := image.Config.Labels[k]
  274. assert.Assert(t, ok)
  275. assert.Assert(t, x == v)
  276. }
  277. }
  278. func TestBuildWithEmptyLayers(t *testing.T) {
  279. dockerfile := `
  280. FROM busybox
  281. COPY 1/ /target/
  282. COPY 2/ /target/
  283. COPY 3/ /target/
  284. `
  285. ctx := context.Background()
  286. source := fakecontext.New(t, "",
  287. fakecontext.WithDockerfile(dockerfile),
  288. fakecontext.WithFile("1/a", "asdf"),
  289. fakecontext.WithFile("2/a", "asdf"),
  290. fakecontext.WithFile("3/a", "asdf"))
  291. defer source.Close()
  292. apiclient := testEnv.APIClient()
  293. resp, err := apiclient.ImageBuild(ctx,
  294. source.AsTarReader(t),
  295. types.ImageBuildOptions{
  296. Remove: true,
  297. ForceRemove: true,
  298. })
  299. assert.NilError(t, err)
  300. _, err = io.Copy(ioutil.Discard, resp.Body)
  301. resp.Body.Close()
  302. assert.NilError(t, err)
  303. }
  304. // TestBuildMultiStageOnBuild checks that ONBUILD commands are applied to
  305. // multiple subsequent stages
  306. // #35652
  307. func TestBuildMultiStageOnBuild(t *testing.T) {
  308. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.33"), "broken in earlier versions")
  309. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  310. defer setupTest(t)()
  311. // test both metadata and layer based commands as they may be implemented differently
  312. dockerfile := `FROM busybox AS stage1
  313. ONBUILD RUN echo 'foo' >somefile
  314. ONBUILD ENV bar=baz
  315. FROM stage1
  316. # fails if ONBUILD RUN fails
  317. RUN cat somefile
  318. FROM stage1
  319. RUN cat somefile`
  320. ctx := context.Background()
  321. source := fakecontext.New(t, "",
  322. fakecontext.WithDockerfile(dockerfile))
  323. defer source.Close()
  324. apiclient := testEnv.APIClient()
  325. resp, err := apiclient.ImageBuild(ctx,
  326. source.AsTarReader(t),
  327. types.ImageBuildOptions{
  328. Remove: true,
  329. ForceRemove: true,
  330. })
  331. out := bytes.NewBuffer(nil)
  332. assert.NilError(t, err)
  333. _, err = io.Copy(out, resp.Body)
  334. resp.Body.Close()
  335. assert.NilError(t, err)
  336. assert.Check(t, is.Contains(out.String(), "Successfully built"))
  337. imageIDs, err := getImageIDsFromBuild(out.Bytes())
  338. assert.NilError(t, err)
  339. assert.Check(t, is.Equal(3, len(imageIDs)))
  340. image, _, err := apiclient.ImageInspectWithRaw(context.Background(), imageIDs[2])
  341. assert.NilError(t, err)
  342. assert.Check(t, is.Contains(image.Config.Env, "bar=baz"))
  343. }
  344. // #35403 #36122
  345. func TestBuildUncleanTarFilenames(t *testing.T) {
  346. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
  347. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  348. ctx := context.TODO()
  349. defer setupTest(t)()
  350. dockerfile := `FROM scratch
  351. COPY foo /
  352. FROM scratch
  353. COPY bar /`
  354. buf := bytes.NewBuffer(nil)
  355. w := tar.NewWriter(buf)
  356. writeTarRecord(t, w, "Dockerfile", dockerfile)
  357. writeTarRecord(t, w, "../foo", "foocontents0")
  358. writeTarRecord(t, w, "/bar", "barcontents0")
  359. err := w.Close()
  360. assert.NilError(t, err)
  361. apiclient := testEnv.APIClient()
  362. resp, err := apiclient.ImageBuild(ctx,
  363. buf,
  364. types.ImageBuildOptions{
  365. Remove: true,
  366. ForceRemove: true,
  367. })
  368. out := bytes.NewBuffer(nil)
  369. assert.NilError(t, err)
  370. _, err = io.Copy(out, resp.Body)
  371. resp.Body.Close()
  372. assert.NilError(t, err)
  373. // repeat with changed data should not cause cache hits
  374. buf = bytes.NewBuffer(nil)
  375. w = tar.NewWriter(buf)
  376. writeTarRecord(t, w, "Dockerfile", dockerfile)
  377. writeTarRecord(t, w, "../foo", "foocontents1")
  378. writeTarRecord(t, w, "/bar", "barcontents1")
  379. err = w.Close()
  380. assert.NilError(t, err)
  381. resp, err = apiclient.ImageBuild(ctx,
  382. buf,
  383. types.ImageBuildOptions{
  384. Remove: true,
  385. ForceRemove: true,
  386. })
  387. out = bytes.NewBuffer(nil)
  388. assert.NilError(t, err)
  389. _, err = io.Copy(out, resp.Body)
  390. resp.Body.Close()
  391. assert.NilError(t, err)
  392. assert.Assert(t, !strings.Contains(out.String(), "Using cache"))
  393. }
  394. // docker/for-linux#135
  395. // #35641
  396. func TestBuildMultiStageLayerLeak(t *testing.T) {
  397. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  398. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.37"), "broken in earlier versions")
  399. ctx := context.TODO()
  400. defer setupTest(t)()
  401. // all commands need to match until COPY
  402. dockerfile := `FROM busybox
  403. WORKDIR /foo
  404. COPY foo .
  405. FROM busybox
  406. WORKDIR /foo
  407. COPY bar .
  408. RUN [ -f bar ]
  409. RUN [ ! -f foo ]
  410. `
  411. source := fakecontext.New(t, "",
  412. fakecontext.WithFile("foo", "0"),
  413. fakecontext.WithFile("bar", "1"),
  414. fakecontext.WithDockerfile(dockerfile))
  415. defer source.Close()
  416. apiclient := testEnv.APIClient()
  417. resp, err := apiclient.ImageBuild(ctx,
  418. source.AsTarReader(t),
  419. types.ImageBuildOptions{
  420. Remove: true,
  421. ForceRemove: true,
  422. })
  423. out := bytes.NewBuffer(nil)
  424. assert.NilError(t, err)
  425. _, err = io.Copy(out, resp.Body)
  426. resp.Body.Close()
  427. assert.NilError(t, err)
  428. assert.Check(t, is.Contains(out.String(), "Successfully built"))
  429. }
  430. // #37581
  431. func TestBuildWithHugeFile(t *testing.T) {
  432. skip.If(t, testEnv.OSType == "windows")
  433. ctx := context.TODO()
  434. defer setupTest(t)()
  435. dockerfile := `FROM busybox
  436. # create a sparse file with size over 8GB
  437. 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 && \
  438. ls -la rnd && du -sk rnd`
  439. buf := bytes.NewBuffer(nil)
  440. w := tar.NewWriter(buf)
  441. writeTarRecord(t, w, "Dockerfile", dockerfile)
  442. err := w.Close()
  443. assert.NilError(t, err)
  444. apiclient := testEnv.APIClient()
  445. resp, err := apiclient.ImageBuild(ctx,
  446. buf,
  447. types.ImageBuildOptions{
  448. Remove: true,
  449. ForceRemove: true,
  450. })
  451. out := bytes.NewBuffer(nil)
  452. assert.NilError(t, err)
  453. _, err = io.Copy(out, resp.Body)
  454. resp.Body.Close()
  455. assert.NilError(t, err)
  456. assert.Check(t, is.Contains(out.String(), "Successfully built"))
  457. }
  458. func TestBuildWithEmptyDockerfile(t *testing.T) {
  459. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
  460. ctx := context.TODO()
  461. defer setupTest(t)()
  462. tests := []struct {
  463. name string
  464. dockerfile string
  465. expectedErr string
  466. }{
  467. {
  468. name: "empty-dockerfile",
  469. dockerfile: "",
  470. expectedErr: "cannot be empty",
  471. },
  472. {
  473. name: "empty-lines-dockerfile",
  474. dockerfile: `
  475. `,
  476. expectedErr: "file with no instructions",
  477. },
  478. {
  479. name: "comment-only-dockerfile",
  480. dockerfile: `# this is a comment`,
  481. expectedErr: "file with no instructions",
  482. },
  483. }
  484. apiclient := testEnv.APIClient()
  485. for _, tc := range tests {
  486. tc := tc
  487. t.Run(tc.name, func(t *testing.T) {
  488. t.Parallel()
  489. buf := bytes.NewBuffer(nil)
  490. w := tar.NewWriter(buf)
  491. writeTarRecord(t, w, "Dockerfile", tc.dockerfile)
  492. err := w.Close()
  493. assert.NilError(t, err)
  494. _, err = apiclient.ImageBuild(ctx,
  495. buf,
  496. types.ImageBuildOptions{
  497. Remove: true,
  498. ForceRemove: true,
  499. })
  500. assert.Check(t, is.Contains(err.Error(), tc.expectedErr))
  501. })
  502. }
  503. }
  504. func TestBuildPreserveOwnership(t *testing.T) {
  505. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME")
  506. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "broken in earlier versions")
  507. ctx := context.Background()
  508. dockerfile, err := ioutil.ReadFile("testdata/Dockerfile.testBuildPreserveOwnership")
  509. assert.NilError(t, err)
  510. source := fakecontext.New(t, "", fakecontext.WithDockerfile(string(dockerfile)))
  511. defer source.Close()
  512. apiclient := testEnv.APIClient()
  513. for _, target := range []string{"copy_from", "copy_from_chowned"} {
  514. t.Run(target, func(t *testing.T) {
  515. resp, err := apiclient.ImageBuild(
  516. ctx,
  517. source.AsTarReader(t),
  518. types.ImageBuildOptions{
  519. Remove: true,
  520. ForceRemove: true,
  521. Target: target,
  522. },
  523. )
  524. assert.NilError(t, err)
  525. out := bytes.NewBuffer(nil)
  526. _, err = io.Copy(out, resp.Body)
  527. _ = resp.Body.Close()
  528. if err != nil {
  529. t.Log(out)
  530. }
  531. assert.NilError(t, err)
  532. })
  533. }
  534. }
  535. func TestBuildPlatformInvalid(t *testing.T) {
  536. skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.40"), "experimental in older versions")
  537. ctx := context.Background()
  538. defer setupTest(t)()
  539. dockerfile := `FROM busybox
  540. `
  541. buf := bytes.NewBuffer(nil)
  542. w := tar.NewWriter(buf)
  543. writeTarRecord(t, w, "Dockerfile", dockerfile)
  544. err := w.Close()
  545. assert.NilError(t, err)
  546. apiclient := testEnv.APIClient()
  547. _, err = apiclient.ImageBuild(ctx,
  548. buf,
  549. types.ImageBuildOptions{
  550. Remove: true,
  551. ForceRemove: true,
  552. Platform: "foobar",
  553. })
  554. assert.Assert(t, err != nil)
  555. assert.ErrorContains(t, err, "unknown operating system or architecture")
  556. assert.Assert(t, errdefs.IsInvalidParameter(err))
  557. }
  558. func writeTarRecord(t *testing.T, w *tar.Writer, fn, contents string) {
  559. err := w.WriteHeader(&tar.Header{
  560. Name: fn,
  561. Mode: 0600,
  562. Size: int64(len(contents)),
  563. Typeflag: '0',
  564. })
  565. assert.NilError(t, err)
  566. _, err = w.Write([]byte(contents))
  567. assert.NilError(t, err)
  568. }
  569. type buildLine struct {
  570. Stream string
  571. Aux struct {
  572. ID string
  573. }
  574. }
  575. func getImageIDsFromBuild(output []byte) ([]string, error) {
  576. var ids []string
  577. for _, line := range bytes.Split(output, []byte("\n")) {
  578. if len(line) == 0 {
  579. continue
  580. }
  581. entry := buildLine{}
  582. if err := json.Unmarshal(line, &entry); err != nil {
  583. return nil, err
  584. }
  585. if entry.Aux.ID != "" {
  586. ids = append(ids, entry.Aux.ID)
  587. }
  588. }
  589. return ids, nil
  590. }