docker_api_build_test.go 15 KB

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