build.go 16 KB

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