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