15 "github.com/docker/cli/cli"
16 "github.com/docker/cli/cli/command"
17 "github.com/docker/cli/cli/command/image/build"
18 "github.com/docker/cli/opts"
19 "github.com/docker/distribution/reference"
20 "github.com/docker/docker/api"
21 "github.com/docker/docker/api/types"
22 "github.com/docker/docker/api/types/container"
23 "github.com/docker/docker/pkg/archive"
24 "github.com/docker/docker/pkg/jsonmessage"
25 "github.com/docker/docker/pkg/progress"
26 "github.com/docker/docker/pkg/streamformatter"
27 "github.com/docker/docker/pkg/urlutil"
28 units "github.com/docker/go-units"
29 "github.com/pkg/errors"
30 "github.com/spf13/cobra"
31 "golang.org/x/net/context"
34 type buildOptions struct {
39 buildArgs opts.ListOpts
40 extraHosts opts.ListOpts
41 ulimits *opts.UlimitOpt
43 memorySwap opts.MemSwapBytes
67 // dockerfileFromStdin returns true when the user specified that the Dockerfile
68 // should be read from stdin instead of a file
69 func (o buildOptions) dockerfileFromStdin() bool {
70 return o.dockerfileName == "-"
73 // contextFromStdin returns true when the user specified that the build context
74 // should be read from stdin
75 func (o buildOptions) contextFromStdin() bool {
76 return o.context == "-"
79 // NewBuildCommand creates a new `docker build` command
80 func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
81 ulimits := make(map[string]*units.Ulimit)
82 options := buildOptions{
83 tags: opts.NewListOpts(validateTag),
84 buildArgs: opts.NewListOpts(opts.ValidateEnv),
85 ulimits: opts.NewUlimitOpt(&ulimits),
86 labels: opts.NewListOpts(opts.ValidateEnv),
87 extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
88 volumes: opts.NewListOpts(nil),
91 cmd := &cobra.Command{
92 Use: "build [OPTIONS] PATH | URL | -",
93 Short: "Build an image from a Dockerfile",
94 Args: cli.ExactArgs(1),
95 RunE: func(cmd *cobra.Command, args []string) error {
96 options.context = args[0]
97 return runBuild(dockerCli, options)
103 flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format")
104 flags.Var(&options.buildArgs, "build-arg", "Set build-time variables")
105 flags.Var(options.ulimits, "ulimit", "Ulimit options")
106 flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')")
107 flags.VarP(&options.memory, "memory", "m", "Memory limit")
108 flags.Var(&options.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap")
109 flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm")
110 flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
111 flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period")
112 flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota")
113 flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
114 flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)")
115 flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
116 flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology")
117 flags.Var(&options.labels, "label", "Set metadata for an image")
118 flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image")
119 flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build")
120 flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers")
121 flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success")
122 flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
123 flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
124 flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip")
125 flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
126 flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build")
127 flags.SetAnnotation("network", "version", []string{"1.25"})
128 flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
129 flags.StringVar(&options.target, "target", "", "Set the target build stage to build.")
130 flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file")
132 command.AddTrustVerificationFlags(flags)
134 flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
135 flags.SetAnnotation("squash", "experimental", nil)
136 flags.SetAnnotation("squash", "version", []string{"1.25"})
137 flags.VarP(&options.volumes, "volume", "v", "Bind mount a volume")
142 // lastProgressOutput is the same as progress.Output except
143 // that it only output with the last update. It is used in
144 // non terminal scenarios to suppress verbose messages
145 type lastProgressOutput struct {
146 output progress.Output
149 // WriteProgress formats progress information from a ProgressReader.
150 func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
151 if !prog.LastUpdate {
155 return out.output.WriteProgress(prog)
159 func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
161 buildCtx io.ReadCloser
162 dockerfileCtx io.ReadCloser
171 if options.dockerfileFromStdin() {
172 if options.contextFromStdin() {
173 return errors.New("invalid argument: can't use stdin for both build context and dockerfile")
175 dockerfileCtx = dockerCli.In()
178 specifiedContext := options.context
179 progBuff = dockerCli.Out()
180 buildBuff = dockerCli.Out()
182 progBuff = bytes.NewBuffer(nil)
183 buildBuff = bytes.NewBuffer(nil)
185 if options.imageIDFile != "" {
186 // Avoid leaving a stale file if we eventually fail
187 if err := os.Remove(options.imageIDFile); err != nil && !os.IsNotExist(err) {
188 return errors.Wrap(err, "Removing image ID file")
193 case options.contextFromStdin():
194 buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
195 case isLocalDir(specifiedContext):
196 contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
197 case urlutil.IsGitURL(specifiedContext):
198 tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)
199 case urlutil.IsURL(specifiedContext):
200 buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
202 return errors.Errorf("unable to prepare context: path %q not found", specifiedContext)
206 if options.quiet && urlutil.IsURL(specifiedContext) {
207 fmt.Fprintln(dockerCli.Err(), progBuff)
209 return errors.Errorf("unable to prepare context: %s", err)
213 defer os.RemoveAll(tempDir)
218 excludes, err := build.ReadDockerignore(contextDir)
223 if err := build.ValidateContextDirectory(contextDir, excludes); err != nil {
224 return errors.Errorf("error checking context: '%s'.", err)
227 // And canonicalize dockerfile name to a platform-independent one
228 relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
230 return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
233 excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
235 compression := archive.Uncompressed
236 if options.compress {
237 compression = archive.Gzip
239 buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
240 Compression: compression,
241 ExcludePatterns: excludes,
248 // replace Dockerfile if added dynamically
249 if dockerfileCtx != nil {
250 buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
256 ctx := context.Background()
258 var resolvedTags []*resolvedTag
259 if command.IsTrusted() {
260 translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
261 return TrustedReference(ctx, dockerCli, ref, nil)
263 // Wrap the tar archive to replace the Dockerfile entry with the rewritten
264 // Dockerfile which uses trusted pulls.
265 buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
268 // Setup an upload progress bar
269 progressOutput := streamformatter.NewProgressOutput(progBuff)
270 if !dockerCli.Out().IsTerminal() {
271 progressOutput = &lastProgressOutput{output: progressOutput}
274 var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
276 authConfigs, _ := dockerCli.GetAllCredentials()
277 buildOptions := types.ImageBuildOptions{
278 Memory: options.memory.Value(),
279 MemorySwap: options.memorySwap.Value(),
280 Tags: options.tags.GetAll(),
281 SuppressOutput: options.quiet,
282 NoCache: options.noCache,
284 ForceRemove: options.forceRm,
285 PullParent: options.pull,
286 Isolation: container.Isolation(options.isolation),
287 CPUSetCPUs: options.cpuSetCpus,
288 CPUSetMems: options.cpuSetMems,
289 CPUShares: options.cpuShares,
290 CPUQuota: options.cpuQuota,
291 CPUPeriod: options.cpuPeriod,
292 CgroupParent: options.cgroupParent,
293 Dockerfile: relDockerfile,
294 ShmSize: options.shmSize.Value(),
295 Ulimits: options.ulimits.GetList(),
296 BuildArgs: dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
297 AuthConfigs: authConfigs,
298 Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
299 CacheFrom: options.cacheFrom,
300 SecurityOpt: options.securityOpt,
301 NetworkMode: options.networkMode,
302 Squash: options.squash,
303 ExtraHosts: options.extraHosts.GetAll(),
304 Target: options.target,
305 Volumes: options.volumes.GetAll(),
308 response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
311 fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
315 defer response.Body.Close()
318 aux := func(auxJSON *json.RawMessage) {
319 var result types.BuildResult
320 if err := json.Unmarshal(*auxJSON, &result); err != nil {
321 fmt.Fprintf(dockerCli.Err(), "Failed to parse aux message: %s", err)
327 err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), aux)
329 if jerr, ok := err.(*jsonmessage.JSONError); ok {
330 // If no error code is set, default to 1
335 fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
337 return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
342 // Windows: show error message about modified file permissions if the
343 // daemon isn't running Windows.
344 if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
345 fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+
346 "image from Windows against a non-Windows Docker host. All files and "+
347 "directories added to build context will have '-rwxr-xr-x' permissions. "+
348 "It is recommended to double check and reset permissions for sensitive "+
349 "files and directories.")
352 // Everything worked so if -q was provided the output from the daemon
353 // should be just the image ID and we'll print that to stdout.
355 imageID = fmt.Sprintf("%s", buildBuff)
356 fmt.Fprintf(dockerCli.Out(), imageID)
359 if options.imageIDFile != "" {
361 return errors.Errorf("Server did not provide an image ID. Cannot write %s", options.imageIDFile)
363 if err := ioutil.WriteFile(options.imageIDFile, []byte(imageID), 0666); err != nil {
367 if command.IsTrusted() {
368 // Since the build was successful, now we must tag any of the resolved
369 // images from the above Dockerfile rewrite.
370 for _, resolved := range resolvedTags {
371 if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil {
380 func isLocalDir(c string) bool {
385 type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error)
387 // validateTag checks if the given image name can be resolved.
388 func validateTag(rawRepo string) (string, error) {
389 _, err := reference.ParseNormalizedNamed(rawRepo)
397 var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
399 // resolvedTag records the repository, tag, and resolved digest reference
400 // from a Dockerfile rewrite.
401 type resolvedTag struct {
402 digestRef reference.Canonical
403 tagRef reference.NamedTagged
406 // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
407 // "FROM <image>" instructions to a digest reference. `translator` is a
408 // function that takes a repository name and tag reference and returns a
409 // trusted digest reference.
410 func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) {
411 scanner := bufio.NewScanner(dockerfile)
412 buf := bytes.NewBuffer(nil)
414 // Scan the lines of the Dockerfile, looking for a "FROM" line.
416 line := scanner.Text()
418 matches := dockerfileFromLinePattern.FindStringSubmatch(line)
419 if matches != nil && matches[1] != api.NoBaseImageSpecifier {
420 // Replace the line with a resolved "FROM repo@digest"
421 var ref reference.Named
422 ref, err = reference.ParseNormalizedNamed(matches[1])
426 ref = reference.TagNameOnly(ref)
427 if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
428 trustedRef, err := translator(ctx, ref)
433 line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", reference.FamiliarString(trustedRef)))
434 resolvedTags = append(resolvedTags, &resolvedTag{
435 digestRef: trustedRef,
441 _, err := fmt.Fprintln(buf, line)
447 return buf.Bytes(), resolvedTags, scanner.Err()
450 // replaceDockerfileTarWrapper wraps the given input tar archive stream and
451 // replaces the entry with the given Dockerfile name with the contents of the
452 // new Dockerfile. Returns a new tar archive stream with the replaced
454 func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
455 pipeReader, pipeWriter := io.Pipe()
457 tarReader := tar.NewReader(inputTarStream)
458 tarWriter := tar.NewWriter(pipeWriter)
460 defer inputTarStream.Close()
463 hdr, err := tarReader.Next()
465 // Signals end of archive.
471 pipeWriter.CloseWithError(err)
475 content := io.Reader(tarReader)
476 if hdr.Name == dockerfileName {
477 // This entry is the Dockerfile. Since the tar archive was
478 // generated from a directory on the local filesystem, the
479 // Dockerfile will only appear once in the archive.
480 var newDockerfile []byte
481 newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator)
483 pipeWriter.CloseWithError(err)
486 hdr.Size = int64(len(newDockerfile))
487 content = bytes.NewBuffer(newDockerfile)
490 if err := tarWriter.WriteHeader(hdr); err != nil {
491 pipeWriter.CloseWithError(err)
495 if _, err := io.Copy(tarWriter, content); err != nil {
496 pipeWriter.CloseWithError(err)