github.com/flavio/docker@v0.1.3-0.20170117145210-f63d1a6eec47/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/docker/api"
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/api/types/container"
    17  	"github.com/docker/docker/builder/dockerignore"
    18  	"github.com/docker/docker/cli"
    19  	"github.com/docker/docker/cli/command"
    20  	"github.com/docker/docker/cli/command/image/build"
    21  	"github.com/docker/docker/opts"
    22  	"github.com/docker/docker/pkg/archive"
    23  	"github.com/docker/docker/pkg/fileutils"
    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  	"github.com/docker/docker/reference"
    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        string
    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.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", "Set the networking mode for the RUN instructions during build")
   110  
   111  	command.AddTrustedFlags(flags, true)
   112  
   113  	flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer")
   114  	flags.SetAnnotation("squash", "experimental", nil)
   115  	flags.SetAnnotation("squash", "version", []string{"1.25"})
   116  
   117  	return cmd
   118  }
   119  
   120  // lastProgressOutput is the same as progress.Output except
   121  // that it only output with the last update. It is used in
   122  // non terminal scenarios to depresss verbose messages
   123  type lastProgressOutput struct {
   124  	output progress.Output
   125  }
   126  
   127  // WriteProgress formats progress information from a ProgressReader.
   128  func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
   129  	if !prog.LastUpdate {
   130  		return nil
   131  	}
   132  
   133  	return out.output.WriteProgress(prog)
   134  }
   135  
   136  func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
   137  
   138  	var (
   139  		buildCtx      io.ReadCloser
   140  		err           error
   141  		contextDir    string
   142  		tempDir       string
   143  		relDockerfile string
   144  		progBuff      io.Writer
   145  		buildBuff     io.Writer
   146  	)
   147  
   148  	specifiedContext := options.context
   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 = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
   159  	case urlutil.IsGitURL(specifiedContext):
   160  		tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)
   161  	case urlutil.IsURL(specifiedContext):
   162  		buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
   163  	default:
   164  		contextDir, relDockerfile, err = build.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 := build.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, nil)
   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  	authConfigs, _ := dockerCli.GetAllCredentials()
   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.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()),
   303  		AuthConfigs:    authConfigs,
   304  		Labels:         runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
   305  		CacheFrom:      options.cacheFrom,
   306  		SecurityOpt:    options.securityOpt,
   307  		NetworkMode:    options.networkMode,
   308  		Squash:         options.squash,
   309  	}
   310  
   311  	response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
   312  	if err != nil {
   313  		if options.quiet {
   314  			fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
   315  		}
   316  		return err
   317  	}
   318  	defer response.Body.Close()
   319  
   320  	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil)
   321  	if err != nil {
   322  		if jerr, ok := err.(*jsonmessage.JSONError); ok {
   323  			// If no error code is set, default to 1
   324  			if jerr.Code == 0 {
   325  				jerr.Code = 1
   326  			}
   327  			if options.quiet {
   328  				fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
   329  			}
   330  			return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
   331  		}
   332  	}
   333  
   334  	// Windows: show error message about modified file permissions if the
   335  	// daemon isn't running Windows.
   336  	if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
   337  		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.`)
   338  	}
   339  
   340  	// Everything worked so if -q was provided the output from the daemon
   341  	// should be just the image ID and we'll print that to stdout.
   342  	if options.quiet {
   343  		fmt.Fprintf(dockerCli.Out(), "%s", buildBuff)
   344  	}
   345  
   346  	if command.IsTrusted() {
   347  		// Since the build was successful, now we must tag any of the resolved
   348  		// images from the above Dockerfile rewrite.
   349  		for _, resolved := range resolvedTags {
   350  			if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil {
   351  				return err
   352  			}
   353  		}
   354  	}
   355  
   356  	return 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.ParseNamed(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  			ref, err := reference.ParseNamed(matches[1])
   396  			if err != nil {
   397  				return nil, nil, err
   398  			}
   399  			ref = reference.WithDefaultTag(ref)
   400  			if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
   401  				trustedRef, err := translator(ctx, ref)
   402  				if err != nil {
   403  					return nil, nil, err
   404  				}
   405  
   406  				line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String()))
   407  				resolvedTags = append(resolvedTags, &resolvedTag{
   408  					digestRef: trustedRef,
   409  					tagRef:    ref,
   410  				})
   411  			}
   412  		}
   413  
   414  		_, err := fmt.Fprintln(buf, line)
   415  		if err != nil {
   416  			return nil, nil, err
   417  		}
   418  	}
   419  
   420  	return buf.Bytes(), resolvedTags, scanner.Err()
   421  }
   422  
   423  // replaceDockerfileTarWrapper wraps the given input tar archive stream and
   424  // replaces the entry with the given Dockerfile name with the contents of the
   425  // new Dockerfile. Returns a new tar archive stream with the replaced
   426  // Dockerfile.
   427  func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
   428  	pipeReader, pipeWriter := io.Pipe()
   429  	go func() {
   430  		tarReader := tar.NewReader(inputTarStream)
   431  		tarWriter := tar.NewWriter(pipeWriter)
   432  
   433  		defer inputTarStream.Close()
   434  
   435  		for {
   436  			hdr, err := tarReader.Next()
   437  			if err == io.EOF {
   438  				// Signals end of archive.
   439  				tarWriter.Close()
   440  				pipeWriter.Close()
   441  				return
   442  			}
   443  			if err != nil {
   444  				pipeWriter.CloseWithError(err)
   445  				return
   446  			}
   447  
   448  			content := io.Reader(tarReader)
   449  			if hdr.Name == dockerfileName {
   450  				// This entry is the Dockerfile. Since the tar archive was
   451  				// generated from a directory on the local filesystem, the
   452  				// Dockerfile will only appear once in the archive.
   453  				var newDockerfile []byte
   454  				newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator)
   455  				if err != nil {
   456  					pipeWriter.CloseWithError(err)
   457  					return
   458  				}
   459  				hdr.Size = int64(len(newDockerfile))
   460  				content = bytes.NewBuffer(newDockerfile)
   461  			}
   462  
   463  			if err := tarWriter.WriteHeader(hdr); err != nil {
   464  				pipeWriter.CloseWithError(err)
   465  				return
   466  			}
   467  
   468  			if _, err := io.Copy(tarWriter, content); err != nil {
   469  				pipeWriter.CloseWithError(err)
   470  				return
   471  			}
   472  		}
   473  	}()
   474  
   475  	return pipeReader
   476  }