evaluator_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. package dockerfile
  2. import (
  3. "io/ioutil"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "github.com/docker/docker/builder"
  9. "github.com/docker/docker/builder/dockerfile/parser"
  10. "github.com/docker/docker/pkg/archive"
  11. "github.com/docker/docker/pkg/reexec"
  12. "github.com/docker/engine-api/types"
  13. "github.com/docker/engine-api/types/container"
  14. )
  15. type dispatchTestCase struct {
  16. name, dockerfile, expectedError string
  17. files map[string]string
  18. }
  19. func init() {
  20. reexec.Init()
  21. }
  22. func initDispatchTestCases() []dispatchTestCase {
  23. dispatchTestCases := []dispatchTestCase{{
  24. name: "copyEmptyWhitespace",
  25. dockerfile: `COPY
  26. quux \
  27. bar`,
  28. expectedError: "COPY requires at least one argument",
  29. },
  30. {
  31. name: "ONBUILD forbidden FROM",
  32. dockerfile: "ONBUILD FROM scratch",
  33. expectedError: "FROM isn't allowed as an ONBUILD trigger",
  34. files: nil,
  35. },
  36. {
  37. name: "ONBUILD forbidden MAINTAINER",
  38. dockerfile: "ONBUILD MAINTAINER docker.io",
  39. expectedError: "MAINTAINER isn't allowed as an ONBUILD trigger",
  40. files: nil,
  41. },
  42. {
  43. name: "ARG two arguments",
  44. dockerfile: "ARG foo bar",
  45. expectedError: "ARG requires exactly one argument definition",
  46. files: nil,
  47. },
  48. {
  49. name: "MAINTAINER unknown flag",
  50. dockerfile: "MAINTAINER --boo joe@example.com",
  51. expectedError: "Unknown flag: boo",
  52. files: nil,
  53. },
  54. {
  55. name: "ADD multiple files to file",
  56. dockerfile: "ADD file1.txt file2.txt test",
  57. expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
  58. files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
  59. },
  60. {
  61. name: "JSON ADD multiple files to file",
  62. dockerfile: `ADD ["file1.txt", "file2.txt", "test"]`,
  63. expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
  64. files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
  65. },
  66. {
  67. name: "Wildcard ADD multiple files to file",
  68. dockerfile: "ADD file*.txt test",
  69. expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
  70. files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
  71. },
  72. {
  73. name: "Wildcard JSON ADD multiple files to file",
  74. dockerfile: `ADD ["file*.txt", "test"]`,
  75. expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
  76. files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
  77. },
  78. {
  79. name: "COPY multiple files to file",
  80. dockerfile: "COPY file1.txt file2.txt test",
  81. expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /",
  82. files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
  83. },
  84. {
  85. name: "JSON COPY multiple files to file",
  86. dockerfile: `COPY ["file1.txt", "file2.txt", "test"]`,
  87. expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /",
  88. files: map[string]string{"file1.txt": "test1", "file2.txt": "test2"},
  89. },
  90. {
  91. name: "ADD multiple files to file with whitespace",
  92. dockerfile: `ADD [ "test file1.txt", "test file2.txt", "test" ]`,
  93. expectedError: "When using ADD with more than one source file, the destination must be a directory and end with a /",
  94. files: map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"},
  95. },
  96. {
  97. name: "COPY multiple files to file with whitespace",
  98. dockerfile: `COPY [ "test file1.txt", "test file2.txt", "test" ]`,
  99. expectedError: "When using COPY with more than one source file, the destination must be a directory and end with a /",
  100. files: map[string]string{"test file1.txt": "test1", "test file2.txt": "test2"},
  101. },
  102. {
  103. name: "COPY wildcard no files",
  104. dockerfile: `COPY file*.txt /tmp/`,
  105. expectedError: "No source files were specified",
  106. files: nil,
  107. },
  108. {
  109. name: "COPY url",
  110. dockerfile: `COPY https://index.docker.io/robots.txt /`,
  111. expectedError: "Source can't be a URL for COPY",
  112. files: nil,
  113. },
  114. {
  115. name: "Chaining ONBUILD",
  116. dockerfile: `ONBUILD ONBUILD RUN touch foobar`,
  117. expectedError: "Chaining ONBUILD via `ONBUILD ONBUILD` isn't allowed",
  118. files: nil,
  119. },
  120. {
  121. name: "Invalid instruction",
  122. dockerfile: `foo bar`,
  123. expectedError: "Unknown instruction: FOO",
  124. files: nil,
  125. }}
  126. return dispatchTestCases
  127. }
  128. func TestDispatch(t *testing.T) {
  129. testCases := initDispatchTestCases()
  130. for _, testCase := range testCases {
  131. executeTestCase(t, testCase)
  132. }
  133. }
  134. func executeTestCase(t *testing.T, testCase dispatchTestCase) {
  135. contextDir, cleanup := createTestTempDir(t, "", "builder-dockerfile-test")
  136. defer cleanup()
  137. for filename, content := range testCase.files {
  138. createTestTempFile(t, contextDir, filename, content, 0777)
  139. }
  140. tarStream, err := archive.Tar(contextDir, archive.Uncompressed)
  141. if err != nil {
  142. t.Fatalf("Error when creating tar stream: %s", err)
  143. }
  144. defer func() {
  145. if err = tarStream.Close(); err != nil {
  146. t.Fatalf("Error when closing tar stream: %s", err)
  147. }
  148. }()
  149. context, err := builder.MakeTarSumContext(tarStream)
  150. if err != nil {
  151. t.Fatalf("Error when creating tar context: %s", err)
  152. }
  153. defer func() {
  154. if err = context.Close(); err != nil {
  155. t.Fatalf("Error when closing tar context: %s", err)
  156. }
  157. }()
  158. r := strings.NewReader(testCase.dockerfile)
  159. d := parser.Directive{}
  160. parser.SetEscapeToken(parser.DefaultEscapeToken, &d)
  161. n, err := parser.Parse(r, &d)
  162. if err != nil {
  163. t.Fatalf("Error when parsing Dockerfile: %s", err)
  164. }
  165. config := &container.Config{}
  166. options := &types.ImageBuildOptions{}
  167. b := &Builder{runConfig: config, options: options, Stdout: ioutil.Discard, context: context}
  168. err = b.dispatch(0, n.Children[0])
  169. if err == nil {
  170. t.Fatalf("No error when executing test %s", testCase.name)
  171. }
  172. if !strings.Contains(err.Error(), testCase.expectedError) {
  173. t.Fatalf("Wrong error message. Should be \"%s\". Got \"%s\"", testCase.expectedError, err.Error())
  174. }
  175. }
  176. // createTestTempDir creates a temporary directory for testing.
  177. // It returns the created path and a cleanup function which is meant to be used as deferred call.
  178. // When an error occurs, it terminates the test.
  179. func createTestTempDir(t *testing.T, dir, prefix string) (string, func()) {
  180. path, err := ioutil.TempDir(dir, prefix)
  181. if err != nil {
  182. t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err)
  183. }
  184. return path, func() {
  185. err = os.RemoveAll(path)
  186. if err != nil {
  187. t.Fatalf("Error when removing directory %s: %s", path, err)
  188. }
  189. }
  190. }
  191. // createTestTempFile creates a temporary file within dir with specific contents and permissions.
  192. // When an error occurs, it terminates the test
  193. func createTestTempFile(t *testing.T, dir, filename, contents string, perm os.FileMode) string {
  194. filePath := filepath.Join(dir, filename)
  195. err := ioutil.WriteFile(filePath, []byte(contents), perm)
  196. if err != nil {
  197. t.Fatalf("Error when creating %s file: %s", filename, err)
  198. }
  199. return filePath
  200. }