github.com/olljanat/moby@v1.13.1/cli/command/image/build.go (about)

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