github.com/zhouyu0/docker-note@v0.0.0-20190722021225-b8d3825084db/builder/dockerfile/internals.go (about)

     1  package dockerfile // import "github.com/docker/docker/builder/dockerfile"
     2  
     3  // internals for handling commands. Covers many areas and a lot of
     4  // non-contiguous functionality. Please read the comments.
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/hex"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"runtime"
    15  	"strings"
    16  
    17  	"github.com/docker/docker/api/types"
    18  	"github.com/docker/docker/api/types/backend"
    19  	"github.com/docker/docker/api/types/container"
    20  	"github.com/docker/docker/builder"
    21  	"github.com/docker/docker/image"
    22  	"github.com/docker/docker/pkg/archive"
    23  	"github.com/docker/docker/pkg/chrootarchive"
    24  	"github.com/docker/docker/pkg/containerfs"
    25  	"github.com/docker/docker/pkg/idtools"
    26  	"github.com/docker/docker/pkg/stringid"
    27  	"github.com/docker/docker/pkg/system"
    28  	"github.com/docker/go-connections/nat"
    29  	"github.com/pkg/errors"
    30  	"github.com/sirupsen/logrus"
    31  )
    32  
    33  // Archiver defines an interface for copying files from one destination to
    34  // another using Tar/Untar.
    35  type Archiver interface {
    36  	TarUntar(src, dst string) error
    37  	UntarPath(src, dst string) error
    38  	CopyWithTar(src, dst string) error
    39  	CopyFileWithTar(src, dst string) error
    40  	IdentityMapping() *idtools.IdentityMapping
    41  }
    42  
    43  // The builder will use the following interfaces if the container fs implements
    44  // these for optimized copies to and from the container.
    45  type extractor interface {
    46  	ExtractArchive(src io.Reader, dst string, opts *archive.TarOptions) error
    47  }
    48  
    49  type archiver interface {
    50  	ArchivePath(src string, opts *archive.TarOptions) (io.ReadCloser, error)
    51  }
    52  
    53  // helper functions to get tar/untar func
    54  func untarFunc(i interface{}) containerfs.UntarFunc {
    55  	if ea, ok := i.(extractor); ok {
    56  		return ea.ExtractArchive
    57  	}
    58  	return chrootarchive.Untar
    59  }
    60  
    61  func tarFunc(i interface{}) containerfs.TarFunc {
    62  	if ap, ok := i.(archiver); ok {
    63  		return ap.ArchivePath
    64  	}
    65  	return archive.TarWithOptions
    66  }
    67  
    68  func (b *Builder) getArchiver(src, dst containerfs.Driver) Archiver {
    69  	t, u := tarFunc(src), untarFunc(dst)
    70  	return &containerfs.Archiver{
    71  		SrcDriver: src,
    72  		DstDriver: dst,
    73  		Tar:       t,
    74  		Untar:     u,
    75  		IDMapping: b.idMapping,
    76  	}
    77  }
    78  
    79  func (b *Builder) commit(dispatchState *dispatchState, comment string) error {
    80  	if b.disableCommit {
    81  		return nil
    82  	}
    83  	if !dispatchState.hasFromImage() {
    84  		return errors.New("Please provide a source image with `from` prior to commit")
    85  	}
    86  
    87  	runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, dispatchState.operatingSystem))
    88  	id, err := b.probeAndCreate(dispatchState, runConfigWithCommentCmd)
    89  	if err != nil || id == "" {
    90  		return err
    91  	}
    92  
    93  	return b.commitContainer(dispatchState, id, runConfigWithCommentCmd)
    94  }
    95  
    96  func (b *Builder) commitContainer(dispatchState *dispatchState, id string, containerConfig *container.Config) error {
    97  	if b.disableCommit {
    98  		return nil
    99  	}
   100  
   101  	commitCfg := backend.CommitConfig{
   102  		Author: dispatchState.maintainer,
   103  		// TODO: this copy should be done by Commit()
   104  		Config:          copyRunConfig(dispatchState.runConfig),
   105  		ContainerConfig: containerConfig,
   106  		ContainerID:     id,
   107  	}
   108  
   109  	imageID, err := b.docker.CommitBuildStep(commitCfg)
   110  	dispatchState.imageID = string(imageID)
   111  	return err
   112  }
   113  
   114  func (b *Builder) exportImage(state *dispatchState, layer builder.RWLayer, parent builder.Image, runConfig *container.Config) error {
   115  	newLayer, err := layer.Commit()
   116  	if err != nil {
   117  		return err
   118  	}
   119  
   120  	// add an image mount without an image so the layer is properly unmounted
   121  	// if there is an error before we can add the full mount with image
   122  	b.imageSources.Add(newImageMount(nil, newLayer))
   123  
   124  	parentImage, ok := parent.(*image.Image)
   125  	if !ok {
   126  		return errors.Errorf("unexpected image type")
   127  	}
   128  
   129  	newImage := image.NewChildImage(parentImage, image.ChildConfig{
   130  		Author:          state.maintainer,
   131  		ContainerConfig: runConfig,
   132  		DiffID:          newLayer.DiffID(),
   133  		Config:          copyRunConfig(state.runConfig),
   134  	}, parentImage.OS)
   135  
   136  	// TODO: it seems strange to marshal this here instead of just passing in the
   137  	// image struct
   138  	config, err := newImage.MarshalJSON()
   139  	if err != nil {
   140  		return errors.Wrap(err, "failed to encode image config")
   141  	}
   142  
   143  	exportedImage, err := b.docker.CreateImage(config, state.imageID)
   144  	if err != nil {
   145  		return errors.Wrapf(err, "failed to export image")
   146  	}
   147  
   148  	state.imageID = exportedImage.ImageID()
   149  	b.imageSources.Add(newImageMount(exportedImage, newLayer))
   150  	return nil
   151  }
   152  
   153  func (b *Builder) performCopy(req dispatchRequest, inst copyInstruction) error {
   154  	state := req.state
   155  	srcHash := getSourceHashFromInfos(inst.infos)
   156  
   157  	var chownComment string
   158  	if inst.chownStr != "" {
   159  		chownComment = fmt.Sprintf("--chown=%s", inst.chownStr)
   160  	}
   161  	commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest)
   162  
   163  	// TODO: should this have been using origPaths instead of srcHash in the comment?
   164  	runConfigWithCommentCmd := copyRunConfig(
   165  		state.runConfig,
   166  		withCmdCommentString(commentStr, state.operatingSystem))
   167  	hit, err := b.probeCache(state, runConfigWithCommentCmd)
   168  	if err != nil || hit {
   169  		return err
   170  	}
   171  
   172  	imageMount, err := b.imageSources.Get(state.imageID, true, req.builder.platform)
   173  	if err != nil {
   174  		return errors.Wrapf(err, "failed to get destination image %q", state.imageID)
   175  	}
   176  
   177  	rwLayer, err := imageMount.NewRWLayer()
   178  	if err != nil {
   179  		return err
   180  	}
   181  	defer rwLayer.Release()
   182  
   183  	destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer, state.operatingSystem)
   184  	if err != nil {
   185  		return err
   186  	}
   187  
   188  	identity := b.idMapping.RootPair()
   189  	// if a chown was requested, perform the steps to get the uid, gid
   190  	// translated (if necessary because of user namespaces), and replace
   191  	// the root pair with the chown pair for copy operations
   192  	if inst.chownStr != "" {
   193  		identity, err = parseChownFlag(b, state, inst.chownStr, destInfo.root.Path(), b.idMapping)
   194  		if err != nil {
   195  			if b.options.Platform != "windows" {
   196  				return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping")
   197  			}
   198  
   199  			return errors.Wrapf(err, "unable to map container user account name to SID")
   200  		}
   201  	}
   202  
   203  	for _, info := range inst.infos {
   204  		opts := copyFileOptions{
   205  			decompress: inst.allowLocalDecompression,
   206  			archiver:   b.getArchiver(info.root, destInfo.root),
   207  			identity:   identity,
   208  		}
   209  		if err := performCopyForInfo(destInfo, info, opts); err != nil {
   210  			return errors.Wrapf(err, "failed to copy files")
   211  		}
   212  	}
   213  	return b.exportImage(state, rwLayer, imageMount.Image(), runConfigWithCommentCmd)
   214  }
   215  
   216  func createDestInfo(workingDir string, inst copyInstruction, rwLayer builder.RWLayer, platform string) (copyInfo, error) {
   217  	// Twiddle the destination when it's a relative path - meaning, make it
   218  	// relative to the WORKINGDIR
   219  	dest, err := normalizeDest(workingDir, inst.dest, platform)
   220  	if err != nil {
   221  		return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName)
   222  	}
   223  
   224  	return copyInfo{root: rwLayer.Root(), path: dest}, nil
   225  }
   226  
   227  // normalizeDest normalises the destination of a COPY/ADD command in a
   228  // platform semantically consistent way.
   229  func normalizeDest(workingDir, requested string, platform string) (string, error) {
   230  	dest := fromSlash(requested, platform)
   231  	endsInSlash := strings.HasSuffix(dest, string(separator(platform)))
   232  
   233  	if platform != "windows" {
   234  		if !path.IsAbs(requested) {
   235  			dest = path.Join("/", filepath.ToSlash(workingDir), dest)
   236  			// Make sure we preserve any trailing slash
   237  			if endsInSlash {
   238  				dest += "/"
   239  			}
   240  		}
   241  		return dest, nil
   242  	}
   243  
   244  	// We are guaranteed that the working directory is already consistent,
   245  	// However, Windows also has, for now, the limitation that ADD/COPY can
   246  	// only be done to the system drive, not any drives that might be present
   247  	// as a result of a bind mount.
   248  	//
   249  	// So... if the path requested is Linux-style absolute (/foo or \\foo),
   250  	// we assume it is the system drive. If it is a Windows-style absolute
   251  	// (DRIVE:\\foo), error if DRIVE is not C. And finally, ensure we
   252  	// strip any configured working directories drive letter so that it
   253  	// can be subsequently legitimately converted to a Windows volume-style
   254  	// pathname.
   255  
   256  	// Not a typo - filepath.IsAbs, not system.IsAbs on this next check as
   257  	// we only want to validate where the DriveColon part has been supplied.
   258  	if filepath.IsAbs(dest) {
   259  		if strings.ToUpper(string(dest[0])) != "C" {
   260  			return "", fmt.Errorf("Windows does not support destinations not on the system drive (C:)")
   261  		}
   262  		dest = dest[2:] // Strip the drive letter
   263  	}
   264  
   265  	// Cannot handle relative where WorkingDir is not the system drive.
   266  	if len(workingDir) > 0 {
   267  		if ((len(workingDir) > 1) && !system.IsAbs(workingDir[2:])) || (len(workingDir) == 1) {
   268  			return "", fmt.Errorf("Current WorkingDir %s is not platform consistent", workingDir)
   269  		}
   270  		if !system.IsAbs(dest) {
   271  			if string(workingDir[0]) != "C" {
   272  				return "", fmt.Errorf("Windows does not support relative paths when WORKDIR is not the system drive")
   273  			}
   274  			dest = filepath.Join(string(os.PathSeparator), workingDir[2:], dest)
   275  			// Make sure we preserve any trailing slash
   276  			if endsInSlash {
   277  				dest += string(os.PathSeparator)
   278  			}
   279  		}
   280  	}
   281  	return dest, nil
   282  }
   283  
   284  // For backwards compat, if there's just one info then use it as the
   285  // cache look-up string, otherwise hash 'em all into one
   286  func getSourceHashFromInfos(infos []copyInfo) string {
   287  	if len(infos) == 1 {
   288  		return infos[0].hash
   289  	}
   290  	var hashs []string
   291  	for _, info := range infos {
   292  		hashs = append(hashs, info.hash)
   293  	}
   294  	return hashStringSlice("multi", hashs)
   295  }
   296  
   297  func hashStringSlice(prefix string, slice []string) string {
   298  	hasher := sha256.New()
   299  	hasher.Write([]byte(strings.Join(slice, ",")))
   300  	return prefix + ":" + hex.EncodeToString(hasher.Sum(nil))
   301  }
   302  
   303  type runConfigModifier func(*container.Config)
   304  
   305  func withCmd(cmd []string) runConfigModifier {
   306  	return func(runConfig *container.Config) {
   307  		runConfig.Cmd = cmd
   308  	}
   309  }
   310  
   311  // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for
   312  // why there are two almost identical versions of this.
   313  func withCmdComment(comment string, platform string) runConfigModifier {
   314  	return func(runConfig *container.Config) {
   315  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment)
   316  	}
   317  }
   318  
   319  // withCmdCommentString exists to maintain compatibility with older versions.
   320  // A few instructions (workdir, copy, add) used a nop comment that is a single arg
   321  // where as all the other instructions used a two arg comment string. This
   322  // function implements the single arg version.
   323  func withCmdCommentString(comment string, platform string) runConfigModifier {
   324  	return func(runConfig *container.Config) {
   325  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment)
   326  	}
   327  }
   328  
   329  func withEnv(env []string) runConfigModifier {
   330  	return func(runConfig *container.Config) {
   331  		runConfig.Env = env
   332  	}
   333  }
   334  
   335  // withEntrypointOverride sets an entrypoint on runConfig if the command is
   336  // not empty. The entrypoint is left unmodified if command is empty.
   337  //
   338  // The dockerfile RUN instruction expect to run without an entrypoint
   339  // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate
   340  // will change a []string{""} entrypoint to nil, so we probe the cache with the
   341  // nil entrypoint.
   342  func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier {
   343  	return func(runConfig *container.Config) {
   344  		if len(cmd) > 0 {
   345  			runConfig.Entrypoint = entrypoint
   346  		}
   347  	}
   348  }
   349  
   350  // withoutHealthcheck disables healthcheck.
   351  //
   352  // The dockerfile RUN instruction expect to run without healthcheck
   353  // so the runConfig Healthcheck needs to be disabled.
   354  func withoutHealthcheck() runConfigModifier {
   355  	return func(runConfig *container.Config) {
   356  		runConfig.Healthcheck = &container.HealthConfig{
   357  			Test: []string{"NONE"},
   358  		}
   359  	}
   360  }
   361  
   362  func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config {
   363  	copy := *runConfig
   364  	copy.Cmd = copyStringSlice(runConfig.Cmd)
   365  	copy.Env = copyStringSlice(runConfig.Env)
   366  	copy.Entrypoint = copyStringSlice(runConfig.Entrypoint)
   367  	copy.OnBuild = copyStringSlice(runConfig.OnBuild)
   368  	copy.Shell = copyStringSlice(runConfig.Shell)
   369  
   370  	if copy.Volumes != nil {
   371  		copy.Volumes = make(map[string]struct{}, len(runConfig.Volumes))
   372  		for k, v := range runConfig.Volumes {
   373  			copy.Volumes[k] = v
   374  		}
   375  	}
   376  
   377  	if copy.ExposedPorts != nil {
   378  		copy.ExposedPorts = make(nat.PortSet, len(runConfig.ExposedPorts))
   379  		for k, v := range runConfig.ExposedPorts {
   380  			copy.ExposedPorts[k] = v
   381  		}
   382  	}
   383  
   384  	if copy.Labels != nil {
   385  		copy.Labels = make(map[string]string, len(runConfig.Labels))
   386  		for k, v := range runConfig.Labels {
   387  			copy.Labels[k] = v
   388  		}
   389  	}
   390  
   391  	for _, modifier := range modifiers {
   392  		modifier(&copy)
   393  	}
   394  	return &copy
   395  }
   396  
   397  func copyStringSlice(orig []string) []string {
   398  	if orig == nil {
   399  		return nil
   400  	}
   401  	return append([]string{}, orig...)
   402  }
   403  
   404  // getShell is a helper function which gets the right shell for prefixing the
   405  // shell-form of RUN, ENTRYPOINT and CMD instructions
   406  func getShell(c *container.Config, os string) []string {
   407  	if 0 == len(c.Shell) {
   408  		return append([]string{}, defaultShellForOS(os)[:]...)
   409  	}
   410  	return append([]string{}, c.Shell[:]...)
   411  }
   412  
   413  func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
   414  	cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig)
   415  	if cachedID == "" || err != nil {
   416  		return false, err
   417  	}
   418  	fmt.Fprint(b.Stdout, " ---> Using cache\n")
   419  
   420  	dispatchState.imageID = cachedID
   421  	return true, nil
   422  }
   423  
   424  var defaultLogConfig = container.LogConfig{Type: "none"}
   425  
   426  func (b *Builder) probeAndCreate(dispatchState *dispatchState, runConfig *container.Config) (string, error) {
   427  	if hit, err := b.probeCache(dispatchState, runConfig); err != nil || hit {
   428  		return "", err
   429  	}
   430  	return b.create(runConfig)
   431  }
   432  
   433  func (b *Builder) create(runConfig *container.Config) (string, error) {
   434  	logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)
   435  
   436  	isWCOW := runtime.GOOS == "windows" && b.platform != nil && b.platform.OS == "windows"
   437  	hostConfig := hostConfigFromOptions(b.options, isWCOW)
   438  	container, err := b.containerManager.Create(runConfig, hostConfig)
   439  	if err != nil {
   440  		return "", err
   441  	}
   442  	// TODO: could this be moved into containerManager.Create() ?
   443  	for _, warning := range container.Warnings {
   444  		fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning)
   445  	}
   446  	fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(container.ID))
   447  	return container.ID, nil
   448  }
   449  
   450  func hostConfigFromOptions(options *types.ImageBuildOptions, isWCOW bool) *container.HostConfig {
   451  	resources := container.Resources{
   452  		CgroupParent: options.CgroupParent,
   453  		CPUShares:    options.CPUShares,
   454  		CPUPeriod:    options.CPUPeriod,
   455  		CPUQuota:     options.CPUQuota,
   456  		CpusetCpus:   options.CPUSetCPUs,
   457  		CpusetMems:   options.CPUSetMems,
   458  		Memory:       options.Memory,
   459  		MemorySwap:   options.MemorySwap,
   460  		Ulimits:      options.Ulimits,
   461  	}
   462  
   463  	hc := &container.HostConfig{
   464  		SecurityOpt: options.SecurityOpt,
   465  		Isolation:   options.Isolation,
   466  		ShmSize:     options.ShmSize,
   467  		Resources:   resources,
   468  		NetworkMode: container.NetworkMode(options.NetworkMode),
   469  		// Set a log config to override any default value set on the daemon
   470  		LogConfig:  defaultLogConfig,
   471  		ExtraHosts: options.ExtraHosts,
   472  	}
   473  
   474  	// For WCOW, the default of 20GB hard-coded in the platform
   475  	// is too small for builder scenarios where many users are
   476  	// using RUN statements to install large amounts of data.
   477  	// Use 127GB as that's the default size of a VHD in Hyper-V.
   478  	if isWCOW {
   479  		hc.StorageOpt = make(map[string]string)
   480  		hc.StorageOpt["size"] = "127GB"
   481  	}
   482  
   483  	return hc
   484  }
   485  
   486  // fromSlash works like filepath.FromSlash but with a given OS platform field
   487  func fromSlash(path, platform string) string {
   488  	if platform == "windows" {
   489  		return strings.Replace(path, "/", "\\", -1)
   490  	}
   491  	return path
   492  }
   493  
   494  // separator returns a OS path separator for the given OS platform
   495  func separator(platform string) byte {
   496  	if platform == "windows" {
   497  		return '\\'
   498  	}
   499  	return '/'
   500  }