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