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