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