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. "github.com/docker/distribution/reference"
  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/dockerignore"
  17. "github.com/docker/docker/cli"
  18. "github.com/docker/docker/cli/command"
  19. "github.com/docker/docker/cli/command/image/build"
  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. runconfigopts "github.com/docker/docker/runconfig/opts"
  28. units "github.com/docker/go-units"
  29. "github.com/spf13/cobra"
  30. "golang.org/x/net/context"
  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 *opts.UlimitOpt
  39. memory string
  40. memorySwap string
  41. shmSize opts.MemBytes
  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(opts.ValidateEnv),
  66. ulimits: opts.NewUlimitOpt(&ulimits),
  67. labels: opts.NewListOpts(opts.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.Var(&options.shmSize, "shm-size", "Size of /dev/shm")
  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", "Set the networking mode for the RUN instructions during build")
  103. flags.SetAnnotation("network", "version", []string{"1.25"})
  104. command.AddTrustVerificationFlags(flags)
  105. flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
  106. flags.SetAnnotation("squash", "experimental", nil)
  107. flags.SetAnnotation("squash", "version", []string{"1.25"})
  108. return cmd
  109. }
  110. // lastProgressOutput is the same as progress.Output except
  111. // that it only output with the last update. It is used in
  112. // non terminal scenarios to depresss verbose messages
  113. type lastProgressOutput struct {
  114. output progress.Output
  115. }
  116. // WriteProgress formats progress information from a ProgressReader.
  117. func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
  118. if !prog.LastUpdate {
  119. return nil
  120. }
  121. return out.output.WriteProgress(prog)
  122. }
  123. func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
  124. var (
  125. buildCtx io.ReadCloser
  126. err error
  127. contextDir string
  128. tempDir string
  129. relDockerfile string
  130. progBuff io.Writer
  131. buildBuff io.Writer
  132. )
  133. specifiedContext := options.context
  134. progBuff = dockerCli.Out()
  135. buildBuff = dockerCli.Out()
  136. if options.quiet {
  137. progBuff = bytes.NewBuffer(nil)
  138. buildBuff = bytes.NewBuffer(nil)
  139. }
  140. switch {
  141. case specifiedContext == "-":
  142. buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
  143. case isLocalDir(specifiedContext):
  144. contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
  145. case urlutil.IsGitURL(specifiedContext):
  146. tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)
  147. case urlutil.IsURL(specifiedContext):
  148. buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
  149. default:
  150. return fmt.Errorf("unable to prepare context: path %q not found", specifiedContext)
  151. }
  152. if err != nil {
  153. if options.quiet && urlutil.IsURL(specifiedContext) {
  154. fmt.Fprintln(dockerCli.Err(), progBuff)
  155. }
  156. return fmt.Errorf("unable to prepare context: %s", err)
  157. }
  158. if tempDir != "" {
  159. defer os.RemoveAll(tempDir)
  160. contextDir = tempDir
  161. }
  162. if buildCtx == nil {
  163. // And canonicalize dockerfile name to a platform-independent one
  164. relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
  165. if err != nil {
  166. return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
  167. }
  168. f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
  169. if err != nil && !os.IsNotExist(err) {
  170. return err
  171. }
  172. defer f.Close()
  173. var excludes []string
  174. if err == nil {
  175. excludes, err = dockerignore.ReadAll(f)
  176. if err != nil {
  177. return err
  178. }
  179. }
  180. if err := build.ValidateContextDirectory(contextDir, excludes); err != nil {
  181. return fmt.Errorf("Error checking context: '%s'.", err)
  182. }
  183. // If .dockerignore mentions .dockerignore or the Dockerfile
  184. // then make sure we send both files over to the daemon
  185. // because Dockerfile is, obviously, needed no matter what, and
  186. // .dockerignore is needed to know if either one needs to be
  187. // removed. The daemon will remove them for us, if needed, after it
  188. // parses the Dockerfile. Ignore errors here, as they will have been
  189. // caught by validateContextDirectory above.
  190. var includes = []string{"."}
  191. keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
  192. keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
  193. if keepThem1 || keepThem2 {
  194. includes = append(includes, ".dockerignore", relDockerfile)
  195. }
  196. compression := archive.Uncompressed
  197. if options.compress {
  198. compression = archive.Gzip
  199. }
  200. buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
  201. Compression: compression,
  202. ExcludePatterns: excludes,
  203. IncludeFiles: includes,
  204. })
  205. if err != nil {
  206. return err
  207. }
  208. }
  209. ctx := context.Background()
  210. var resolvedTags []*resolvedTag
  211. if command.IsTrusted() {
  212. translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
  213. return TrustedReference(ctx, dockerCli, ref, nil)
  214. }
  215. // Wrap the tar archive to replace the Dockerfile entry with the rewritten
  216. // Dockerfile which uses trusted pulls.
  217. buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
  218. }
  219. // Setup an upload progress bar
  220. progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
  221. if !dockerCli.Out().IsTerminal() {
  222. progressOutput = &lastProgressOutput{output: progressOutput}
  223. }
  224. var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
  225. var memory int64
  226. if options.memory != "" {
  227. parsedMemory, err := units.RAMInBytes(options.memory)
  228. if err != nil {
  229. return err
  230. }
  231. memory = parsedMemory
  232. }
  233. var memorySwap int64
  234. if options.memorySwap != "" {
  235. if options.memorySwap == "-1" {
  236. memorySwap = -1
  237. } else {
  238. parsedMemorySwap, err := units.RAMInBytes(options.memorySwap)
  239. if err != nil {
  240. return err
  241. }
  242. memorySwap = parsedMemorySwap
  243. }
  244. }
  245. authConfigs, _ := dockerCli.GetAllCredentials()
  246. buildOptions := types.ImageBuildOptions{
  247. Memory: memory,
  248. MemorySwap: memorySwap,
  249. Tags: options.tags.GetAll(),
  250. SuppressOutput: options.quiet,
  251. NoCache: options.noCache,
  252. Remove: options.rm,
  253. ForceRemove: options.forceRm,
  254. PullParent: options.pull,
  255. Isolation: container.Isolation(options.isolation),
  256. CPUSetCPUs: options.cpuSetCpus,
  257. CPUSetMems: options.cpuSetMems,
  258. CPUShares: options.cpuShares,
  259. CPUQuota: options.cpuQuota,
  260. CPUPeriod: options.cpuPeriod,
  261. CgroupParent: options.cgroupParent,
  262. Dockerfile: relDockerfile,
  263. ShmSize: options.shmSize.Value(),
  264. Ulimits: options.ulimits.GetList(),
  265. BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()),
  266. AuthConfigs: authConfigs,
  267. Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
  268. CacheFrom: options.cacheFrom,
  269. SecurityOpt: options.securityOpt,
  270. NetworkMode: options.networkMode,
  271. Squash: options.squash,
  272. }
  273. response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
  274. if err != nil {
  275. if options.quiet {
  276. fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
  277. }
  278. return err
  279. }
  280. defer response.Body.Close()
  281. err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil)
  282. if err != nil {
  283. if jerr, ok := err.(*jsonmessage.JSONError); ok {
  284. // If no error code is set, default to 1
  285. if jerr.Code == 0 {
  286. jerr.Code = 1
  287. }
  288. if options.quiet {
  289. fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
  290. }
  291. return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
  292. }
  293. }
  294. // Windows: show error message about modified file permissions if the
  295. // daemon isn't running Windows.
  296. if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
  297. fmt.Fprintln(dockerCli.Out(), `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.`)
  298. }
  299. // Everything worked so if -q was provided the output from the daemon
  300. // should be just the image ID and we'll print that to stdout.
  301. if options.quiet {
  302. fmt.Fprintf(dockerCli.Out(), "%s", buildBuff)
  303. }
  304. if command.IsTrusted() {
  305. // Since the build was successful, now we must tag any of the resolved
  306. // images from the above Dockerfile rewrite.
  307. for _, resolved := range resolvedTags {
  308. if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil {
  309. return err
  310. }
  311. }
  312. }
  313. return nil
  314. }
  315. func isLocalDir(c string) bool {
  316. _, err := os.Stat(c)
  317. return err == 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.ParseNormalizedNamed(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. var ref reference.Named
  349. ref, err = reference.ParseNormalizedNamed(matches[1])
  350. if err != nil {
  351. return nil, nil, err
  352. }
  353. ref = reference.TagNameOnly(ref)
  354. if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
  355. trustedRef, err := translator(ctx, ref)
  356. if err != nil {
  357. return nil, nil, err
  358. }
  359. line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", reference.FamiliarString(trustedRef)))
  360. resolvedTags = append(resolvedTags, &resolvedTag{
  361. digestRef: trustedRef,
  362. tagRef: ref,
  363. })
  364. }
  365. }
  366. _, err := fmt.Fprintln(buf, line)
  367. if err != nil {
  368. return nil, nil, err
  369. }
  370. }
  371. return buf.Bytes(), resolvedTags, scanner.Err()
  372. }
  373. // replaceDockerfileTarWrapper wraps the given input tar archive stream and
  374. // replaces the entry with the given Dockerfile name with the contents of the
  375. // new Dockerfile. Returns a new tar archive stream with the replaced
  376. // Dockerfile.
  377. func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
  378. pipeReader, pipeWriter := io.Pipe()
  379. go func() {
  380. tarReader := tar.NewReader(inputTarStream)
  381. tarWriter := tar.NewWriter(pipeWriter)
  382. defer inputTarStream.Close()
  383. for {
  384. hdr, err := tarReader.Next()
  385. if err == io.EOF {
  386. // Signals end of archive.
  387. tarWriter.Close()
  388. pipeWriter.Close()
  389. return
  390. }
  391. if err != nil {
  392. pipeWriter.CloseWithError(err)
  393. return
  394. }
  395. content := io.Reader(tarReader)
  396. if hdr.Name == dockerfileName {
  397. // This entry is the Dockerfile. Since the tar archive was
  398. // generated from a directory on the local filesystem, the
  399. // Dockerfile will only appear once in the archive.
  400. var newDockerfile []byte
  401. newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator)
  402. if err != nil {
  403. pipeWriter.CloseWithError(err)
  404. return
  405. }
  406. hdr.Size = int64(len(newDockerfile))
  407. content = bytes.NewBuffer(newDockerfile)
  408. }
  409. if err := tarWriter.WriteHeader(hdr); err != nil {
  410. pipeWriter.CloseWithError(err)
  411. return
  412. }
  413. if _, err := io.Copy(tarWriter, content); err != nil {
  414. pipeWriter.CloseWithError(err)
  415. return
  416. }
  417. }
  418. }()
  419. return pipeReader
  420. }