build.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. package image
  2. import (
  3. "archive/tar"
  4. "bufio"
  5. "bytes"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "regexp"
  11. "runtime"
  12. "golang.org/x/net/context"
  13. "github.com/docker/docker/api"
  14. "github.com/docker/docker/api/client"
  15. "github.com/docker/docker/builder"
  16. "github.com/docker/docker/builder/dockerignore"
  17. "github.com/docker/docker/cli"
  18. "github.com/docker/docker/opts"
  19. "github.com/docker/docker/pkg/archive"
  20. "github.com/docker/docker/pkg/fileutils"
  21. "github.com/docker/docker/pkg/jsonmessage"
  22. "github.com/docker/docker/pkg/progress"
  23. "github.com/docker/docker/pkg/streamformatter"
  24. "github.com/docker/docker/pkg/urlutil"
  25. "github.com/docker/docker/reference"
  26. runconfigopts "github.com/docker/docker/runconfig/opts"
  27. "github.com/docker/engine-api/types"
  28. "github.com/docker/engine-api/types/container"
  29. "github.com/docker/go-units"
  30. "github.com/spf13/cobra"
  31. )
  32. type buildOptions struct {
  33. context string
  34. dockerfileName string
  35. tags opts.ListOpts
  36. labels []string
  37. buildArgs opts.ListOpts
  38. ulimits *runconfigopts.UlimitOpt
  39. memory string
  40. memorySwap string
  41. shmSize string
  42. cpuShares int64
  43. cpuPeriod int64
  44. cpuQuota int64
  45. cpuSetCpus string
  46. cpuSetMems string
  47. cgroupParent string
  48. isolation string
  49. quiet bool
  50. noCache bool
  51. rm bool
  52. forceRm bool
  53. pull bool
  54. }
  55. // NewBuildCommand creates a new `docker build` command
  56. func NewBuildCommand(dockerCli *client.DockerCli) *cobra.Command {
  57. ulimits := make(map[string]*units.Ulimit)
  58. options := buildOptions{
  59. tags: opts.NewListOpts(validateTag),
  60. buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv),
  61. ulimits: runconfigopts.NewUlimitOpt(&ulimits),
  62. }
  63. cmd := &cobra.Command{
  64. Use: "build [OPTIONS] PATH | URL | -",
  65. Short: "Build an image from a Dockerfile",
  66. Args: cli.ExactArgs(1),
  67. RunE: func(cmd *cobra.Command, args []string) error {
  68. options.context = args[0]
  69. return runBuild(dockerCli, options)
  70. },
  71. }
  72. flags := cmd.Flags()
  73. flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format")
  74. flags.Var(&options.buildArgs, "build-arg", "Set build-time variables")
  75. flags.Var(options.ulimits, "ulimit", "Ulimit options")
  76. flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
  77. flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit")
  78. flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
  79. flags.StringVar(&options.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB")
  80. flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
  81. flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period")
  82. flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota")
  83. flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
  84. flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
  85. flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
  86. flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology")
  87. flags.StringSliceVar(&options.labels, "label", []string{}, "Set metadata for an image")
  88. flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image")
  89. flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build")
  90. flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers")
  91. flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
  92. flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
  93. client.AddTrustedFlags(flags, true)
  94. return cmd
  95. }
  96. func runBuild(dockerCli *client.DockerCli, options buildOptions) error {
  97. var (
  98. buildCtx io.ReadCloser
  99. err error
  100. )
  101. specifiedContext := options.context
  102. var (
  103. contextDir string
  104. tempDir string
  105. relDockerfile string
  106. progBuff io.Writer
  107. buildBuff io.Writer
  108. )
  109. progBuff = dockerCli.Out()
  110. buildBuff = dockerCli.Out()
  111. if options.quiet {
  112. progBuff = bytes.NewBuffer(nil)
  113. buildBuff = bytes.NewBuffer(nil)
  114. }
  115. switch {
  116. case specifiedContext == "-":
  117. buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName)
  118. case urlutil.IsGitURL(specifiedContext):
  119. tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName)
  120. case urlutil.IsURL(specifiedContext):
  121. buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
  122. default:
  123. contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
  124. }
  125. if err != nil {
  126. if options.quiet && urlutil.IsURL(specifiedContext) {
  127. fmt.Fprintln(dockerCli.Err(), progBuff)
  128. }
  129. return fmt.Errorf("unable to prepare context: %s", err)
  130. }
  131. if tempDir != "" {
  132. defer os.RemoveAll(tempDir)
  133. contextDir = tempDir
  134. }
  135. if buildCtx == nil {
  136. // And canonicalize dockerfile name to a platform-independent one
  137. relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
  138. if err != nil {
  139. return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
  140. }
  141. f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
  142. if err != nil && !os.IsNotExist(err) {
  143. return err
  144. }
  145. var excludes []string
  146. if err == nil {
  147. excludes, err = dockerignore.ReadAll(f)
  148. if err != nil {
  149. return err
  150. }
  151. }
  152. if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil {
  153. return fmt.Errorf("Error checking context: '%s'.", err)
  154. }
  155. // If .dockerignore mentions .dockerignore or the Dockerfile
  156. // then make sure we send both files over to the daemon
  157. // because Dockerfile is, obviously, needed no matter what, and
  158. // .dockerignore is needed to know if either one needs to be
  159. // removed. The daemon will remove them for us, if needed, after it
  160. // parses the Dockerfile. Ignore errors here, as they will have been
  161. // caught by validateContextDirectory above.
  162. var includes = []string{"."}
  163. keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
  164. keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
  165. if keepThem1 || keepThem2 {
  166. includes = append(includes, ".dockerignore", relDockerfile)
  167. }
  168. buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
  169. Compression: archive.Uncompressed,
  170. ExcludePatterns: excludes,
  171. IncludeFiles: includes,
  172. })
  173. if err != nil {
  174. return err
  175. }
  176. }
  177. ctx := context.Background()
  178. var resolvedTags []*resolvedTag
  179. if client.IsTrusted() {
  180. // Wrap the tar archive to replace the Dockerfile entry with the rewritten
  181. // Dockerfile which uses trusted pulls.
  182. buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags)
  183. }
  184. // Setup an upload progress bar
  185. progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
  186. var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
  187. var memory int64
  188. if options.memory != "" {
  189. parsedMemory, err := units.RAMInBytes(options.memory)
  190. if err != nil {
  191. return err
  192. }
  193. memory = parsedMemory
  194. }
  195. var memorySwap int64
  196. if options.memorySwap != "" {
  197. if options.memorySwap == "-1" {
  198. memorySwap = -1
  199. } else {
  200. parsedMemorySwap, err := units.RAMInBytes(options.memorySwap)
  201. if err != nil {
  202. return err
  203. }
  204. memorySwap = parsedMemorySwap
  205. }
  206. }
  207. var shmSize int64
  208. if options.shmSize != "" {
  209. shmSize, err = units.RAMInBytes(options.shmSize)
  210. if err != nil {
  211. return err
  212. }
  213. }
  214. buildOptions := types.ImageBuildOptions{
  215. Memory: memory,
  216. MemorySwap: memorySwap,
  217. Tags: options.tags.GetAll(),
  218. SuppressOutput: options.quiet,
  219. NoCache: options.noCache,
  220. Remove: options.rm,
  221. ForceRemove: options.forceRm,
  222. PullParent: options.pull,
  223. Isolation: container.Isolation(options.isolation),
  224. CPUSetCPUs: options.cpuSetCpus,
  225. CPUSetMems: options.cpuSetMems,
  226. CPUShares: options.cpuShares,
  227. CPUQuota: options.cpuQuota,
  228. CPUPeriod: options.cpuPeriod,
  229. CgroupParent: options.cgroupParent,
  230. Dockerfile: relDockerfile,
  231. ShmSize: shmSize,
  232. Ulimits: options.ulimits.GetList(),
  233. BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()),
  234. AuthConfigs: dockerCli.RetrieveAuthConfigs(),
  235. Labels: runconfigopts.ConvertKVStringsToMap(options.labels),
  236. }
  237. response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
  238. if err != nil {
  239. return err
  240. }
  241. defer response.Body.Close()
  242. err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil)
  243. if err != nil {
  244. if jerr, ok := err.(*jsonmessage.JSONError); ok {
  245. // If no error code is set, default to 1
  246. if jerr.Code == 0 {
  247. jerr.Code = 1
  248. }
  249. if options.quiet {
  250. fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
  251. }
  252. return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
  253. }
  254. }
  255. // Windows: show error message about modified file permissions if the
  256. // daemon isn't running Windows.
  257. if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
  258. fmt.Fprintln(dockerCli.Err(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`)
  259. }
  260. // Everything worked so if -q was provided the output from the daemon
  261. // should be just the image ID and we'll print that to stdout.
  262. if options.quiet {
  263. fmt.Fprintf(dockerCli.Out(), "%s", buildBuff)
  264. }
  265. if client.IsTrusted() {
  266. // Since the build was successful, now we must tag any of the resolved
  267. // images from the above Dockerfile rewrite.
  268. for _, resolved := range resolvedTags {
  269. if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil {
  270. return err
  271. }
  272. }
  273. }
  274. return nil
  275. }
  276. type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error)
  277. // validateTag checks if the given image name can be resolved.
  278. func validateTag(rawRepo string) (string, error) {
  279. _, err := reference.ParseNamed(rawRepo)
  280. if err != nil {
  281. return "", err
  282. }
  283. return rawRepo, nil
  284. }
  285. var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
  286. // resolvedTag records the repository, tag, and resolved digest reference
  287. // from a Dockerfile rewrite.
  288. type resolvedTag struct {
  289. digestRef reference.Canonical
  290. tagRef reference.NamedTagged
  291. }
  292. // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
  293. // "FROM <image>" instructions to a digest reference. `translator` is a
  294. // function that takes a repository name and tag reference and returns a
  295. // trusted digest reference.
  296. func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) {
  297. scanner := bufio.NewScanner(dockerfile)
  298. buf := bytes.NewBuffer(nil)
  299. // Scan the lines of the Dockerfile, looking for a "FROM" line.
  300. for scanner.Scan() {
  301. line := scanner.Text()
  302. matches := dockerfileFromLinePattern.FindStringSubmatch(line)
  303. if matches != nil && matches[1] != api.NoBaseImageSpecifier {
  304. // Replace the line with a resolved "FROM repo@digest"
  305. ref, err := reference.ParseNamed(matches[1])
  306. if err != nil {
  307. return nil, nil, err
  308. }
  309. ref = reference.WithDefaultTag(ref)
  310. if ref, ok := ref.(reference.NamedTagged); ok && client.IsTrusted() {
  311. trustedRef, err := translator(ctx, ref)
  312. if err != nil {
  313. return nil, nil, err
  314. }
  315. line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String()))
  316. resolvedTags = append(resolvedTags, &resolvedTag{
  317. digestRef: trustedRef,
  318. tagRef: ref,
  319. })
  320. }
  321. }
  322. _, err := fmt.Fprintln(buf, line)
  323. if err != nil {
  324. return nil, nil, err
  325. }
  326. }
  327. return buf.Bytes(), resolvedTags, scanner.Err()
  328. }
  329. // replaceDockerfileTarWrapper wraps the given input tar archive stream and
  330. // replaces the entry with the given Dockerfile name with the contents of the
  331. // new Dockerfile. Returns a new tar archive stream with the replaced
  332. // Dockerfile.
  333. func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
  334. pipeReader, pipeWriter := io.Pipe()
  335. go func() {
  336. tarReader := tar.NewReader(inputTarStream)
  337. tarWriter := tar.NewWriter(pipeWriter)
  338. defer inputTarStream.Close()
  339. for {
  340. hdr, err := tarReader.Next()
  341. if err == io.EOF {
  342. // Signals end of archive.
  343. tarWriter.Close()
  344. pipeWriter.Close()
  345. return
  346. }
  347. if err != nil {
  348. pipeWriter.CloseWithError(err)
  349. return
  350. }
  351. var content io.Reader = tarReader
  352. if hdr.Name == dockerfileName {
  353. // This entry is the Dockerfile. Since the tar archive was
  354. // generated from a directory on the local filesystem, the
  355. // Dockerfile will only appear once in the archive.
  356. var newDockerfile []byte
  357. newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator)
  358. if err != nil {
  359. pipeWriter.CloseWithError(err)
  360. return
  361. }
  362. hdr.Size = int64(len(newDockerfile))
  363. content = bytes.NewBuffer(newDockerfile)
  364. }
  365. if err := tarWriter.WriteHeader(hdr); err != nil {
  366. pipeWriter.CloseWithError(err)
  367. return
  368. }
  369. if _, err := io.Copy(tarWriter, content); err != nil {
  370. pipeWriter.CloseWithError(err)
  371. return
  372. }
  373. }
  374. }()
  375. return pipeReader
  376. }