github.com/DaoCloud/dao@v0.0.0-20161212064103-c3dbfd13ee36/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         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  }
    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  		labels:    opts.NewListOpts(runconfigopts.ValidateEnv),
    68  	}
    69  
    70  	cmd := &cobra.Command{
    71  		Use:   "build [OPTIONS] PATH | URL | -",
    72  		Short: "从一个Dockerfile构建新的镜像",
    73  		Args:  cli.ExactArgs(1),
    74  		RunE: func(cmd *cobra.Command, args []string) error {
    75  			options.context = args[0]
    76  			return runBuild(dockerCli, options)
    77  		},
    78  	}
    79  
    80  	flags := cmd.Flags()
    81  
    82  	flags.VarP(&options.tags, "tag", "t", "镜像名称以及可选的标签,若指定标签,格式为:'名称:标签' ")
    83  	flags.Var(&options.buildArgs, "build-arg", "设置构建时的环境变量")
    84  	flags.Var(options.ulimits, "ulimit", "设置Ulimit参数")
    85  	flags.StringVarP(&options.dockerfileName, "file", "f", "", "Dockerfile的名称(默认为当前目录下的Dockerfile文件路径)")
    86  	flags.StringVarP(&options.memory, "memory", "m", "", "内存限制")
    87  	flags.StringVar(&options.memorySwap, "memory-swap", "", "交换内存限制 等于 实际内存 + 交换区内存: '-1' 代表启用不受限的交换区内存")
    88  	flags.StringVar(&options.shmSize, "shm-size", "", "共享内存/dev/shm 的大小, 默认值是64MB")
    89  	flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU计算资源的值(相对值)")
    90  	flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "限制CPU绝对公平调度算法(CFS)的时间周期")
    91  	flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "限制CPU绝对公平调度算法(CFS)的时间限额")
    92  	flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "允许容器执行的CPU核指定(0-3,0,1): 0-3代表运行运行在0,1,2,3这4个核上")
    93  	flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "允许容器执行的CPU内存所在核指定(0-3,0,1): 0-3代表运行运行在0,1,2,3这4个核上")
    94  	flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "容器的可选cgroup父路径")
    95  	flags.StringVar(&options.isolation, "isolation", "", "设置容器的隔离策略")
    96  	flags.Var(&options.labels, "label", "为一个镜像设置元数据")
    97  	flags.BoolVar(&options.noCache, "no-cache", false, "构建镜像时不使用镜像缓存")
    98  	flags.BoolVar(&options.rm, "rm", true, "成狗构建后删除中间容器")
    99  	flags.BoolVar(&options.forceRm, "force-rm", false, "总是产出中间结果的容器")
   100  	flags.BoolVarP(&options.quiet, "quiet", "q", false, "压缩构建输出,并在构建成功时打印镜像ID")
   101  	flags.BoolVar(&options.pull, "pull", false, "总是尝试下拉最新版本的镜像")
   102  
   103  	client.AddTrustedFlags(flags, true)
   104  
   105  	return cmd
   106  }
   107  
   108  func runBuild(dockerCli *client.DockerCli, options buildOptions) error {
   109  
   110  	var (
   111  		buildCtx io.ReadCloser
   112  		err      error
   113  	)
   114  
   115  	specifiedContext := options.context
   116  
   117  	var (
   118  		contextDir    string
   119  		tempDir       string
   120  		relDockerfile string
   121  		progBuff      io.Writer
   122  		buildBuff     io.Writer
   123  	)
   124  
   125  	progBuff = dockerCli.Out()
   126  	buildBuff = dockerCli.Out()
   127  	if options.quiet {
   128  		progBuff = bytes.NewBuffer(nil)
   129  		buildBuff = bytes.NewBuffer(nil)
   130  	}
   131  
   132  	switch {
   133  	case specifiedContext == "-":
   134  		buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName)
   135  	case urlutil.IsGitURL(specifiedContext):
   136  		tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName)
   137  	case urlutil.IsURL(specifiedContext):
   138  		buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
   139  	default:
   140  		contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
   141  	}
   142  
   143  	if err != nil {
   144  		if options.quiet && urlutil.IsURL(specifiedContext) {
   145  			fmt.Fprintln(dockerCli.Err(), progBuff)
   146  		}
   147  		return fmt.Errorf("准备构建上下文失败: %s", err)
   148  	}
   149  
   150  	if tempDir != "" {
   151  		defer os.RemoveAll(tempDir)
   152  		contextDir = tempDir
   153  	}
   154  
   155  	if buildCtx == nil {
   156  		// And canonicalize dockerfile name to a platform-independent one
   157  		relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
   158  		if err != nil {
   159  			return fmt.Errorf("不能规范Dockerfile路径 %s: %v", relDockerfile, err)
   160  		}
   161  
   162  		f, err := os.Open(filepath.Join(contextDir, ".dockerignore"))
   163  		if err != nil && !os.IsNotExist(err) {
   164  			return err
   165  		}
   166  
   167  		var excludes []string
   168  		if err == nil {
   169  			excludes, err = dockerignore.ReadAll(f)
   170  			if err != nil {
   171  				return err
   172  			}
   173  		}
   174  
   175  		if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil {
   176  			return fmt.Errorf("检验构建上下文出错: '%s'.", err)
   177  		}
   178  
   179  		// If .dockerignore mentions .dockerignore or the Dockerfile
   180  		// then make sure we send both files over to the daemon
   181  		// because Dockerfile is, obviously, needed no matter what, and
   182  		// .dockerignore is needed to know if either one needs to be
   183  		// removed. The daemon will remove them for us, if needed, after it
   184  		// parses the Dockerfile. Ignore errors here, as they will have been
   185  		// caught by validateContextDirectory above.
   186  		var includes = []string{"."}
   187  		keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
   188  		keepThem2, _ := fileutils.Matches(relDockerfile, excludes)
   189  		if keepThem1 || keepThem2 {
   190  			includes = append(includes, ".dockerignore", relDockerfile)
   191  		}
   192  
   193  		buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
   194  			Compression:     archive.Uncompressed,
   195  			ExcludePatterns: excludes,
   196  			IncludeFiles:    includes,
   197  		})
   198  		if err != nil {
   199  			return err
   200  		}
   201  	}
   202  
   203  	ctx := context.Background()
   204  
   205  	var resolvedTags []*resolvedTag
   206  	if client.IsTrusted() {
   207  		// Wrap the tar archive to replace the Dockerfile entry with the rewritten
   208  		// Dockerfile which uses trusted pulls.
   209  		buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags)
   210  	}
   211  
   212  	// Setup an upload progress bar
   213  	progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
   214  
   215  	var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
   216  
   217  	var memory int64
   218  	if options.memory != "" {
   219  		parsedMemory, err := units.RAMInBytes(options.memory)
   220  		if err != nil {
   221  			return err
   222  		}
   223  		memory = parsedMemory
   224  	}
   225  
   226  	var memorySwap int64
   227  	if options.memorySwap != "" {
   228  		if options.memorySwap == "-1" {
   229  			memorySwap = -1
   230  		} else {
   231  			parsedMemorySwap, err := units.RAMInBytes(options.memorySwap)
   232  			if err != nil {
   233  				return err
   234  			}
   235  			memorySwap = parsedMemorySwap
   236  		}
   237  	}
   238  
   239  	var shmSize int64
   240  	if options.shmSize != "" {
   241  		shmSize, err = units.RAMInBytes(options.shmSize)
   242  		if err != nil {
   243  			return err
   244  		}
   245  	}
   246  
   247  	buildOptions := types.ImageBuildOptions{
   248  		Memory:         memory,
   249  		MemorySwap:     memorySwap,
   250  		Tags:           options.tags.GetAll(),
   251  		SuppressOutput: options.quiet,
   252  		NoCache:        options.noCache,
   253  		Remove:         options.rm,
   254  		ForceRemove:    options.forceRm,
   255  		PullParent:     options.pull,
   256  		Isolation:      container.Isolation(options.isolation),
   257  		CPUSetCPUs:     options.cpuSetCpus,
   258  		CPUSetMems:     options.cpuSetMems,
   259  		CPUShares:      options.cpuShares,
   260  		CPUQuota:       options.cpuQuota,
   261  		CPUPeriod:      options.cpuPeriod,
   262  		CgroupParent:   options.cgroupParent,
   263  		Dockerfile:     relDockerfile,
   264  		ShmSize:        shmSize,
   265  		Ulimits:        options.ulimits.GetList(),
   266  		BuildArgs:      runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()),
   267  		AuthConfigs:    dockerCli.RetrieveAuthConfigs(),
   268  		Labels:         runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
   269  	}
   270  
   271  	response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	defer response.Body.Close()
   276  
   277  	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.OutFd(), dockerCli.IsTerminalOut(), nil)
   278  	if err != nil {
   279  		if jerr, ok := err.(*jsonmessage.JSONError); ok {
   280  			// If no error code is set, default to 1
   281  			if jerr.Code == 0 {
   282  				jerr.Code = 1
   283  			}
   284  			if options.quiet {
   285  				fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff)
   286  			}
   287  			return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code}
   288  		}
   289  	}
   290  
   291  	// Windows: show error message about modified file permissions if the
   292  	// daemon isn't running Windows.
   293  	if response.OSType != "windows" && runtime.GOOS == "windows" {
   294  		fmt.Fprintln(dockerCli.Err(), `安全警告: 您在一个非Windows的Docker引擎上构建Windows机器的Docker的镜像。所有添加到构建上下文的文件和目录将添加'-rwxr-xr-x'权限。推荐您为敏感的文件和目录进行权限的重复查验。`)
   295  	}
   296  
   297  	// Everything worked so if -q was provided the output from the daemon
   298  	// should be just the image ID and we'll print that to stdout.
   299  	if options.quiet {
   300  		fmt.Fprintf(dockerCli.Out(), "%s", buildBuff)
   301  	}
   302  
   303  	if client.IsTrusted() {
   304  		// Since the build was successful, now we must tag any of the resolved
   305  		// images from the above Dockerfile rewrite.
   306  		for _, resolved := range resolvedTags {
   307  			if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil {
   308  				return err
   309  			}
   310  		}
   311  	}
   312  
   313  	return nil
   314  }
   315  
   316  type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error)
   317  
   318  // validateTag checks if the given image name can be resolved.
   319  func validateTag(rawRepo string) (string, error) {
   320  	_, err := reference.ParseNamed(rawRepo)
   321  	if err != nil {
   322  		return "", err
   323  	}
   324  
   325  	return rawRepo, nil
   326  }
   327  
   328  var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
   329  
   330  // resolvedTag records the repository, tag, and resolved digest reference
   331  // from a Dockerfile rewrite.
   332  type resolvedTag struct {
   333  	digestRef reference.Canonical
   334  	tagRef    reference.NamedTagged
   335  }
   336  
   337  // rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in
   338  // "FROM <image>" instructions to a digest reference. `translator` is a
   339  // function that takes a repository name and tag reference and returns a
   340  // trusted digest reference.
   341  func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) {
   342  	scanner := bufio.NewScanner(dockerfile)
   343  	buf := bytes.NewBuffer(nil)
   344  
   345  	// Scan the lines of the Dockerfile, looking for a "FROM" line.
   346  	for scanner.Scan() {
   347  		line := scanner.Text()
   348  
   349  		matches := dockerfileFromLinePattern.FindStringSubmatch(line)
   350  		if matches != nil && matches[1] != api.NoBaseImageSpecifier {
   351  			// Replace the line with a resolved "FROM repo@digest"
   352  			ref, err := reference.ParseNamed(matches[1])
   353  			if err != nil {
   354  				return nil, nil, err
   355  			}
   356  			ref = reference.WithDefaultTag(ref)
   357  			if ref, ok := ref.(reference.NamedTagged); ok && client.IsTrusted() {
   358  				trustedRef, err := translator(ctx, ref)
   359  				if err != nil {
   360  					return nil, nil, err
   361  				}
   362  
   363  				line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String()))
   364  				resolvedTags = append(resolvedTags, &resolvedTag{
   365  					digestRef: trustedRef,
   366  					tagRef:    ref,
   367  				})
   368  			}
   369  		}
   370  
   371  		_, err := fmt.Fprintln(buf, line)
   372  		if err != nil {
   373  			return nil, nil, err
   374  		}
   375  	}
   376  
   377  	return buf.Bytes(), resolvedTags, scanner.Err()
   378  }
   379  
   380  // replaceDockerfileTarWrapper wraps the given input tar archive stream and
   381  // replaces the entry with the given Dockerfile name with the contents of the
   382  // new Dockerfile. Returns a new tar archive stream with the replaced
   383  // Dockerfile.
   384  func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser {
   385  	pipeReader, pipeWriter := io.Pipe()
   386  	go func() {
   387  		tarReader := tar.NewReader(inputTarStream)
   388  		tarWriter := tar.NewWriter(pipeWriter)
   389  
   390  		defer inputTarStream.Close()
   391  
   392  		for {
   393  			hdr, err := tarReader.Next()
   394  			if err == io.EOF {
   395  				// Signals end of archive.
   396  				tarWriter.Close()
   397  				pipeWriter.Close()
   398  				return
   399  			}
   400  			if err != nil {
   401  				pipeWriter.CloseWithError(err)
   402  				return
   403  			}
   404  
   405  			var content io.Reader = tarReader
   406  			if hdr.Name == dockerfileName {
   407  				// This entry is the Dockerfile. Since the tar archive was
   408  				// generated from a directory on the local filesystem, the
   409  				// Dockerfile will only appear once in the archive.
   410  				var newDockerfile []byte
   411  				newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator)
   412  				if err != nil {
   413  					pipeWriter.CloseWithError(err)
   414  					return
   415  				}
   416  				hdr.Size = int64(len(newDockerfile))
   417  				content = bytes.NewBuffer(newDockerfile)
   418  			}
   419  
   420  			if err := tarWriter.WriteHeader(hdr); err != nil {
   421  				pipeWriter.CloseWithError(err)
   422  				return
   423  			}
   424  
   425  			if _, err := io.Copy(tarWriter, content); err != nil {
   426  				pipeWriter.CloseWithError(err)
   427  				return
   428  			}
   429  		}
   430  	}()
   431  
   432  	return pipeReader
   433  }