github.com/jwhonce/docker@v0.6.7-0.20190327063223-da823cf3a5a3/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  		}
   208  		if !inst.preserveOwnership {
   209  			opts.identity = &identity
   210  		}
   211  		if err := performCopyForInfo(destInfo, info, opts); err != nil {
   212  			return errors.Wrapf(err, "failed to copy files")
   213  		}
   214  	}
   215  	return b.exportImage(state, rwLayer, imageMount.Image(), runConfigWithCommentCmd)
   216  }
   217  
   218  func createDestInfo(workingDir string, inst copyInstruction, rwLayer builder.RWLayer, platform string) (copyInfo, error) {
   219  	// Twiddle the destination when it's a relative path - meaning, make it
   220  	// relative to the WORKINGDIR
   221  	dest, err := normalizeDest(workingDir, inst.dest, platform)
   222  	if err != nil {
   223  		return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName)
   224  	}
   225  
   226  	return copyInfo{root: rwLayer.Root(), path: dest}, nil
   227  }
   228  
   229  // normalizeDest normalises the destination of a COPY/ADD command in a
   230  // platform semantically consistent way.
   231  func normalizeDest(workingDir, requested string, platform string) (string, error) {
   232  	dest := fromSlash(requested, platform)
   233  	endsInSlash := strings.HasSuffix(dest, string(separator(platform)))
   234  
   235  	if platform != "windows" {
   236  		if !path.IsAbs(requested) {
   237  			dest = path.Join("/", filepath.ToSlash(workingDir), dest)
   238  			// Make sure we preserve any trailing slash
   239  			if endsInSlash {
   240  				dest += "/"
   241  			}
   242  		}
   243  		return dest, nil
   244  	}
   245  
   246  	// We are guaranteed that the working directory is already consistent,
   247  	// However, Windows also has, for now, the limitation that ADD/COPY can
   248  	// only be done to the system drive, not any drives that might be present
   249  	// as a result of a bind mount.
   250  	//
   251  	// So... if the path requested is Linux-style absolute (/foo or \\foo),
   252  	// we assume it is the system drive. If it is a Windows-style absolute
   253  	// (DRIVE:\\foo), error if DRIVE is not C. And finally, ensure we
   254  	// strip any configured working directories drive letter so that it
   255  	// can be subsequently legitimately converted to a Windows volume-style
   256  	// pathname.
   257  
   258  	// Not a typo - filepath.IsAbs, not system.IsAbs on this next check as
   259  	// we only want to validate where the DriveColon part has been supplied.
   260  	if filepath.IsAbs(dest) {
   261  		if strings.ToUpper(string(dest[0])) != "C" {
   262  			return "", fmt.Errorf("Windows does not support destinations not on the system drive (C:)")
   263  		}
   264  		dest = dest[2:] // Strip the drive letter
   265  	}
   266  
   267  	// Cannot handle relative where WorkingDir is not the system drive.
   268  	if len(workingDir) > 0 {
   269  		if ((len(workingDir) > 1) && !system.IsAbs(workingDir[2:])) || (len(workingDir) == 1) {
   270  			return "", fmt.Errorf("Current WorkingDir %s is not platform consistent", workingDir)
   271  		}
   272  		if !system.IsAbs(dest) {
   273  			if string(workingDir[0]) != "C" {
   274  				return "", fmt.Errorf("Windows does not support relative paths when WORKDIR is not the system drive")
   275  			}
   276  			dest = filepath.Join(string(os.PathSeparator), workingDir[2:], dest)
   277  			// Make sure we preserve any trailing slash
   278  			if endsInSlash {
   279  				dest += string(os.PathSeparator)
   280  			}
   281  		}
   282  	}
   283  	return dest, nil
   284  }
   285  
   286  // For backwards compat, if there's just one info then use it as the
   287  // cache look-up string, otherwise hash 'em all into one
   288  func getSourceHashFromInfos(infos []copyInfo) string {
   289  	if len(infos) == 1 {
   290  		return infos[0].hash
   291  	}
   292  	var hashs []string
   293  	for _, info := range infos {
   294  		hashs = append(hashs, info.hash)
   295  	}
   296  	return hashStringSlice("multi", hashs)
   297  }
   298  
   299  func hashStringSlice(prefix string, slice []string) string {
   300  	hasher := sha256.New()
   301  	hasher.Write([]byte(strings.Join(slice, ",")))
   302  	return prefix + ":" + hex.EncodeToString(hasher.Sum(nil))
   303  }
   304  
   305  type runConfigModifier func(*container.Config)
   306  
   307  func withCmd(cmd []string) runConfigModifier {
   308  	return func(runConfig *container.Config) {
   309  		runConfig.Cmd = cmd
   310  	}
   311  }
   312  
   313  func withArgsEscaped(argsEscaped bool) runConfigModifier {
   314  	return func(runConfig *container.Config) {
   315  		runConfig.ArgsEscaped = argsEscaped
   316  	}
   317  }
   318  
   319  // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for
   320  // why there are two almost identical versions of this.
   321  func withCmdComment(comment string, platform string) runConfigModifier {
   322  	return func(runConfig *container.Config) {
   323  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment)
   324  	}
   325  }
   326  
   327  // withCmdCommentString exists to maintain compatibility with older versions.
   328  // A few instructions (workdir, copy, add) used a nop comment that is a single arg
   329  // where as all the other instructions used a two arg comment string. This
   330  // function implements the single arg version.
   331  func withCmdCommentString(comment string, platform string) runConfigModifier {
   332  	return func(runConfig *container.Config) {
   333  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment)
   334  	}
   335  }
   336  
   337  func withEnv(env []string) runConfigModifier {
   338  	return func(runConfig *container.Config) {
   339  		runConfig.Env = env
   340  	}
   341  }
   342  
   343  // withEntrypointOverride sets an entrypoint on runConfig if the command is
   344  // not empty. The entrypoint is left unmodified if command is empty.
   345  //
   346  // The dockerfile RUN instruction expect to run without an entrypoint
   347  // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate
   348  // will change a []string{""} entrypoint to nil, so we probe the cache with the
   349  // nil entrypoint.
   350  func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier {
   351  	return func(runConfig *container.Config) {
   352  		if len(cmd) > 0 {
   353  			runConfig.Entrypoint = entrypoint
   354  		}
   355  	}
   356  }
   357  
   358  // withoutHealthcheck disables healthcheck.
   359  //
   360  // The dockerfile RUN instruction expect to run without healthcheck
   361  // so the runConfig Healthcheck needs to be disabled.
   362  func withoutHealthcheck() runConfigModifier {
   363  	return func(runConfig *container.Config) {
   364  		runConfig.Healthcheck = &container.HealthConfig{
   365  			Test: []string{"NONE"},
   366  		}
   367  	}
   368  }
   369  
   370  func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config {
   371  	copy := *runConfig
   372  	copy.Cmd = copyStringSlice(runConfig.Cmd)
   373  	copy.Env = copyStringSlice(runConfig.Env)
   374  	copy.Entrypoint = copyStringSlice(runConfig.Entrypoint)
   375  	copy.OnBuild = copyStringSlice(runConfig.OnBuild)
   376  	copy.Shell = copyStringSlice(runConfig.Shell)
   377  
   378  	if copy.Volumes != nil {
   379  		copy.Volumes = make(map[string]struct{}, len(runConfig.Volumes))
   380  		for k, v := range runConfig.Volumes {
   381  			copy.Volumes[k] = v
   382  		}
   383  	}
   384  
   385  	if copy.ExposedPorts != nil {
   386  		copy.ExposedPorts = make(nat.PortSet, len(runConfig.ExposedPorts))
   387  		for k, v := range runConfig.ExposedPorts {
   388  			copy.ExposedPorts[k] = v
   389  		}
   390  	}
   391  
   392  	if copy.Labels != nil {
   393  		copy.Labels = make(map[string]string, len(runConfig.Labels))
   394  		for k, v := range runConfig.Labels {
   395  			copy.Labels[k] = v
   396  		}
   397  	}
   398  
   399  	for _, modifier := range modifiers {
   400  		modifier(&copy)
   401  	}
   402  	return &copy
   403  }
   404  
   405  func copyStringSlice(orig []string) []string {
   406  	if orig == nil {
   407  		return nil
   408  	}
   409  	return append([]string{}, orig...)
   410  }
   411  
   412  // getShell is a helper function which gets the right shell for prefixing the
   413  // shell-form of RUN, ENTRYPOINT and CMD instructions
   414  func getShell(c *container.Config, os string) []string {
   415  	if 0 == len(c.Shell) {
   416  		return append([]string{}, defaultShellForOS(os)[:]...)
   417  	}
   418  	return append([]string{}, c.Shell[:]...)
   419  }
   420  
   421  func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
   422  	cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig)
   423  	if cachedID == "" || err != nil {
   424  		return false, err
   425  	}
   426  	fmt.Fprint(b.Stdout, " ---> Using cache\n")
   427  
   428  	dispatchState.imageID = cachedID
   429  	return true, nil
   430  }
   431  
   432  var defaultLogConfig = container.LogConfig{Type: "none"}
   433  
   434  func (b *Builder) probeAndCreate(dispatchState *dispatchState, runConfig *container.Config) (string, error) {
   435  	if hit, err := b.probeCache(dispatchState, runConfig); err != nil || hit {
   436  		return "", err
   437  	}
   438  	return b.create(runConfig)
   439  }
   440  
   441  func (b *Builder) create(runConfig *container.Config) (string, error) {
   442  	logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)
   443  
   444  	isWCOW := runtime.GOOS == "windows" && b.platform != nil && b.platform.OS == "windows"
   445  	hostConfig := hostConfigFromOptions(b.options, isWCOW)
   446  	container, err := b.containerManager.Create(runConfig, hostConfig)
   447  	if err != nil {
   448  		return "", err
   449  	}
   450  	// TODO: could this be moved into containerManager.Create() ?
   451  	for _, warning := range container.Warnings {
   452  		fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning)
   453  	}
   454  	fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(container.ID))
   455  	return container.ID, nil
   456  }
   457  
   458  func hostConfigFromOptions(options *types.ImageBuildOptions, isWCOW bool) *container.HostConfig {
   459  	resources := container.Resources{
   460  		CgroupParent: options.CgroupParent,
   461  		CPUShares:    options.CPUShares,
   462  		CPUPeriod:    options.CPUPeriod,
   463  		CPUQuota:     options.CPUQuota,
   464  		CpusetCpus:   options.CPUSetCPUs,
   465  		CpusetMems:   options.CPUSetMems,
   466  		Memory:       options.Memory,
   467  		MemorySwap:   options.MemorySwap,
   468  		Ulimits:      options.Ulimits,
   469  	}
   470  
   471  	hc := &container.HostConfig{
   472  		SecurityOpt: options.SecurityOpt,
   473  		Isolation:   options.Isolation,
   474  		ShmSize:     options.ShmSize,
   475  		Resources:   resources,
   476  		NetworkMode: container.NetworkMode(options.NetworkMode),
   477  		// Set a log config to override any default value set on the daemon
   478  		LogConfig:  defaultLogConfig,
   479  		ExtraHosts: options.ExtraHosts,
   480  	}
   481  
   482  	// For WCOW, the default of 20GB hard-coded in the platform
   483  	// is too small for builder scenarios where many users are
   484  	// using RUN statements to install large amounts of data.
   485  	// Use 127GB as that's the default size of a VHD in Hyper-V.
   486  	if isWCOW {
   487  		hc.StorageOpt = make(map[string]string)
   488  		hc.StorageOpt["size"] = "127GB"
   489  	}
   490  
   491  	return hc
   492  }
   493  
   494  // fromSlash works like filepath.FromSlash but with a given OS platform field
   495  func fromSlash(path, platform string) string {
   496  	if platform == "windows" {
   497  		return strings.Replace(path, "/", "\\", -1)
   498  	}
   499  	return path
   500  }
   501  
   502  // separator returns a OS path separator for the given OS platform
   503  func separator(platform string) byte {
   504  	if platform == "windows" {
   505  		return '\\'
   506  	}
   507  	return '/'
   508  }