github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/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  	"strings"
    15  
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/backend"
    18  	"github.com/docker/docker/api/types/container"
    19  	"github.com/docker/docker/builder"
    20  	"github.com/docker/docker/image"
    21  	"github.com/docker/docker/pkg/archive"
    22  	"github.com/docker/docker/pkg/chrootarchive"
    23  	"github.com/docker/docker/pkg/containerfs"
    24  	"github.com/docker/docker/pkg/idtools"
    25  	"github.com/docker/docker/pkg/stringid"
    26  	"github.com/docker/docker/pkg/system"
    27  	"github.com/docker/go-connections/nat"
    28  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    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  	parentImage, ok := parent.(*image.Image)
   121  	if !ok {
   122  		return errors.Errorf("unexpected image type")
   123  	}
   124  
   125  	platform := &specs.Platform{
   126  		OS:           parentImage.OS,
   127  		Architecture: parentImage.Architecture,
   128  		Variant:      parentImage.Variant,
   129  	}
   130  
   131  	// add an image mount without an image so the layer is properly unmounted
   132  	// if there is an error before we can add the full mount with image
   133  	b.imageSources.Add(newImageMount(nil, newLayer), platform)
   134  
   135  	newImage := image.NewChildImage(parentImage, image.ChildConfig{
   136  		Author:          state.maintainer,
   137  		ContainerConfig: runConfig,
   138  		DiffID:          newLayer.DiffID(),
   139  		Config:          copyRunConfig(state.runConfig),
   140  	}, parentImage.OS)
   141  
   142  	// TODO: it seems strange to marshal this here instead of just passing in the
   143  	// image struct
   144  	config, err := newImage.MarshalJSON()
   145  	if err != nil {
   146  		return errors.Wrap(err, "failed to encode image config")
   147  	}
   148  
   149  	exportedImage, err := b.docker.CreateImage(config, state.imageID)
   150  	if err != nil {
   151  		return errors.Wrapf(err, "failed to export image")
   152  	}
   153  
   154  	state.imageID = exportedImage.ImageID()
   155  	b.imageSources.Add(newImageMount(exportedImage, newLayer), platform)
   156  	return nil
   157  }
   158  
   159  func (b *Builder) performCopy(req dispatchRequest, inst copyInstruction) error {
   160  	state := req.state
   161  	srcHash := getSourceHashFromInfos(inst.infos)
   162  
   163  	var chownComment string
   164  	if inst.chownStr != "" {
   165  		chownComment = fmt.Sprintf("--chown=%s", inst.chownStr)
   166  	}
   167  	commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest)
   168  
   169  	// TODO: should this have been using origPaths instead of srcHash in the comment?
   170  	runConfigWithCommentCmd := copyRunConfig(
   171  		state.runConfig,
   172  		withCmdCommentString(commentStr, state.operatingSystem))
   173  	hit, err := b.probeCache(state, runConfigWithCommentCmd)
   174  	if err != nil || hit {
   175  		return err
   176  	}
   177  
   178  	imageMount, err := b.imageSources.Get(state.imageID, true, req.builder.platform)
   179  	if err != nil {
   180  		return errors.Wrapf(err, "failed to get destination image %q", state.imageID)
   181  	}
   182  
   183  	rwLayer, err := imageMount.NewRWLayer()
   184  	if err != nil {
   185  		return err
   186  	}
   187  	defer rwLayer.Release()
   188  
   189  	destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer, state.operatingSystem)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	identity := b.idMapping.RootPair()
   195  	// if a chown was requested, perform the steps to get the uid, gid
   196  	// translated (if necessary because of user namespaces), and replace
   197  	// the root pair with the chown pair for copy operations
   198  	if inst.chownStr != "" {
   199  		identity, err = parseChownFlag(b, state, inst.chownStr, destInfo.root.Path(), b.idMapping)
   200  		if err != nil {
   201  			if b.options.Platform != "windows" {
   202  				return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping")
   203  			}
   204  
   205  			return errors.Wrapf(err, "unable to map container user account name to SID")
   206  		}
   207  	}
   208  
   209  	for _, info := range inst.infos {
   210  		opts := copyFileOptions{
   211  			decompress: inst.allowLocalDecompression,
   212  			archiver:   b.getArchiver(info.root, destInfo.root),
   213  		}
   214  		if !inst.preserveOwnership {
   215  			opts.identity = &identity
   216  		}
   217  		if err := performCopyForInfo(destInfo, info, opts); err != nil {
   218  			return errors.Wrapf(err, "failed to copy files")
   219  		}
   220  	}
   221  	return b.exportImage(state, rwLayer, imageMount.Image(), runConfigWithCommentCmd)
   222  }
   223  
   224  func createDestInfo(workingDir string, inst copyInstruction, rwLayer builder.RWLayer, platform string) (copyInfo, error) {
   225  	// Twiddle the destination when it's a relative path - meaning, make it
   226  	// relative to the WORKINGDIR
   227  	dest, err := normalizeDest(workingDir, inst.dest, platform)
   228  	if err != nil {
   229  		return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName)
   230  	}
   231  
   232  	return copyInfo{root: rwLayer.Root(), path: dest}, nil
   233  }
   234  
   235  // normalizeDest normalises the destination of a COPY/ADD command in a
   236  // platform semantically consistent way.
   237  func normalizeDest(workingDir, requested string, platform string) (string, error) {
   238  	dest := fromSlash(requested, platform)
   239  	endsInSlash := strings.HasSuffix(dest, string(separator(platform)))
   240  
   241  	if platform != "windows" {
   242  		if !path.IsAbs(requested) {
   243  			dest = path.Join("/", filepath.ToSlash(workingDir), dest)
   244  			// Make sure we preserve any trailing slash
   245  			if endsInSlash {
   246  				dest += "/"
   247  			}
   248  		}
   249  		return dest, nil
   250  	}
   251  
   252  	// We are guaranteed that the working directory is already consistent,
   253  	// However, Windows also has, for now, the limitation that ADD/COPY can
   254  	// only be done to the system drive, not any drives that might be present
   255  	// as a result of a bind mount.
   256  	//
   257  	// So... if the path requested is Linux-style absolute (/foo or \\foo),
   258  	// we assume it is the system drive. If it is a Windows-style absolute
   259  	// (DRIVE:\\foo), error if DRIVE is not C. And finally, ensure we
   260  	// strip any configured working directories drive letter so that it
   261  	// can be subsequently legitimately converted to a Windows volume-style
   262  	// pathname.
   263  
   264  	// Not a typo - filepath.IsAbs, not system.IsAbs on this next check as
   265  	// we only want to validate where the DriveColon part has been supplied.
   266  	if filepath.IsAbs(dest) {
   267  		if strings.ToUpper(string(dest[0])) != "C" {
   268  			return "", fmt.Errorf("Windows does not support destinations not on the system drive (C:)")
   269  		}
   270  		dest = dest[2:] // Strip the drive letter
   271  	}
   272  
   273  	// Cannot handle relative where WorkingDir is not the system drive.
   274  	if len(workingDir) > 0 {
   275  		if ((len(workingDir) > 1) && !system.IsAbs(workingDir[2:])) || (len(workingDir) == 1) {
   276  			return "", fmt.Errorf("Current WorkingDir %s is not platform consistent", workingDir)
   277  		}
   278  		if !system.IsAbs(dest) {
   279  			if string(workingDir[0]) != "C" {
   280  				return "", fmt.Errorf("Windows does not support relative paths when WORKDIR is not the system drive")
   281  			}
   282  			dest = filepath.Join(string(os.PathSeparator), workingDir[2:], dest)
   283  			// Make sure we preserve any trailing slash
   284  			if endsInSlash {
   285  				dest += string(os.PathSeparator)
   286  			}
   287  		}
   288  	}
   289  	return dest, nil
   290  }
   291  
   292  // For backwards compat, if there's just one info then use it as the
   293  // cache look-up string, otherwise hash 'em all into one
   294  func getSourceHashFromInfos(infos []copyInfo) string {
   295  	if len(infos) == 1 {
   296  		return infos[0].hash
   297  	}
   298  	var hashs []string
   299  	for _, info := range infos {
   300  		hashs = append(hashs, info.hash)
   301  	}
   302  	return hashStringSlice("multi", hashs)
   303  }
   304  
   305  func hashStringSlice(prefix string, slice []string) string {
   306  	hasher := sha256.New()
   307  	hasher.Write([]byte(strings.Join(slice, ",")))
   308  	return prefix + ":" + hex.EncodeToString(hasher.Sum(nil))
   309  }
   310  
   311  type runConfigModifier func(*container.Config)
   312  
   313  func withCmd(cmd []string) runConfigModifier {
   314  	return func(runConfig *container.Config) {
   315  		runConfig.Cmd = cmd
   316  	}
   317  }
   318  
   319  func withArgsEscaped(argsEscaped bool) runConfigModifier {
   320  	return func(runConfig *container.Config) {
   321  		runConfig.ArgsEscaped = argsEscaped
   322  	}
   323  }
   324  
   325  // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for
   326  // why there are two almost identical versions of this.
   327  func withCmdComment(comment string, platform string) runConfigModifier {
   328  	return func(runConfig *container.Config) {
   329  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment)
   330  	}
   331  }
   332  
   333  // withCmdCommentString exists to maintain compatibility with older versions.
   334  // A few instructions (workdir, copy, add) used a nop comment that is a single arg
   335  // where as all the other instructions used a two arg comment string. This
   336  // function implements the single arg version.
   337  func withCmdCommentString(comment string, platform string) runConfigModifier {
   338  	return func(runConfig *container.Config) {
   339  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment)
   340  	}
   341  }
   342  
   343  func withEnv(env []string) runConfigModifier {
   344  	return func(runConfig *container.Config) {
   345  		runConfig.Env = env
   346  	}
   347  }
   348  
   349  // withEntrypointOverride sets an entrypoint on runConfig if the command is
   350  // not empty. The entrypoint is left unmodified if command is empty.
   351  //
   352  // The dockerfile RUN instruction expect to run without an entrypoint
   353  // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate
   354  // will change a []string{""} entrypoint to nil, so we probe the cache with the
   355  // nil entrypoint.
   356  func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier {
   357  	return func(runConfig *container.Config) {
   358  		if len(cmd) > 0 {
   359  			runConfig.Entrypoint = entrypoint
   360  		}
   361  	}
   362  }
   363  
   364  // withoutHealthcheck disables healthcheck.
   365  //
   366  // The dockerfile RUN instruction expect to run without healthcheck
   367  // so the runConfig Healthcheck needs to be disabled.
   368  func withoutHealthcheck() runConfigModifier {
   369  	return func(runConfig *container.Config) {
   370  		runConfig.Healthcheck = &container.HealthConfig{
   371  			Test: []string{"NONE"},
   372  		}
   373  	}
   374  }
   375  
   376  func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config {
   377  	copy := *runConfig
   378  	copy.Cmd = copyStringSlice(runConfig.Cmd)
   379  	copy.Env = copyStringSlice(runConfig.Env)
   380  	copy.Entrypoint = copyStringSlice(runConfig.Entrypoint)
   381  	copy.OnBuild = copyStringSlice(runConfig.OnBuild)
   382  	copy.Shell = copyStringSlice(runConfig.Shell)
   383  
   384  	if copy.Volumes != nil {
   385  		copy.Volumes = make(map[string]struct{}, len(runConfig.Volumes))
   386  		for k, v := range runConfig.Volumes {
   387  			copy.Volumes[k] = v
   388  		}
   389  	}
   390  
   391  	if copy.ExposedPorts != nil {
   392  		copy.ExposedPorts = make(nat.PortSet, len(runConfig.ExposedPorts))
   393  		for k, v := range runConfig.ExposedPorts {
   394  			copy.ExposedPorts[k] = v
   395  		}
   396  	}
   397  
   398  	if copy.Labels != nil {
   399  		copy.Labels = make(map[string]string, len(runConfig.Labels))
   400  		for k, v := range runConfig.Labels {
   401  			copy.Labels[k] = v
   402  		}
   403  	}
   404  
   405  	for _, modifier := range modifiers {
   406  		modifier(&copy)
   407  	}
   408  	return &copy
   409  }
   410  
   411  func copyStringSlice(orig []string) []string {
   412  	if orig == nil {
   413  		return nil
   414  	}
   415  	return append([]string{}, orig...)
   416  }
   417  
   418  // getShell is a helper function which gets the right shell for prefixing the
   419  // shell-form of RUN, ENTRYPOINT and CMD instructions
   420  func getShell(c *container.Config, os string) []string {
   421  	if 0 == len(c.Shell) {
   422  		return append([]string{}, defaultShellForOS(os)[:]...)
   423  	}
   424  	return append([]string{}, c.Shell[:]...)
   425  }
   426  
   427  func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
   428  	cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig)
   429  	if cachedID == "" || err != nil {
   430  		return false, err
   431  	}
   432  	fmt.Fprint(b.Stdout, " ---> Using cache\n")
   433  
   434  	dispatchState.imageID = cachedID
   435  	return true, nil
   436  }
   437  
   438  var defaultLogConfig = container.LogConfig{Type: "none"}
   439  
   440  func (b *Builder) probeAndCreate(dispatchState *dispatchState, runConfig *container.Config) (string, error) {
   441  	if hit, err := b.probeCache(dispatchState, runConfig); err != nil || hit {
   442  		return "", err
   443  	}
   444  	return b.create(runConfig)
   445  }
   446  
   447  func (b *Builder) create(runConfig *container.Config) (string, error) {
   448  	logrus.Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)
   449  
   450  	hostConfig := hostConfigFromOptions(b.options)
   451  	container, err := b.containerManager.Create(runConfig, hostConfig)
   452  	if err != nil {
   453  		return "", err
   454  	}
   455  	// TODO: could this be moved into containerManager.Create() ?
   456  	for _, warning := range container.Warnings {
   457  		fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning)
   458  	}
   459  	fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(container.ID))
   460  	return container.ID, nil
   461  }
   462  
   463  func hostConfigFromOptions(options *types.ImageBuildOptions) *container.HostConfig {
   464  	resources := container.Resources{
   465  		CgroupParent: options.CgroupParent,
   466  		CPUShares:    options.CPUShares,
   467  		CPUPeriod:    options.CPUPeriod,
   468  		CPUQuota:     options.CPUQuota,
   469  		CpusetCpus:   options.CPUSetCPUs,
   470  		CpusetMems:   options.CPUSetMems,
   471  		Memory:       options.Memory,
   472  		MemorySwap:   options.MemorySwap,
   473  		Ulimits:      options.Ulimits,
   474  	}
   475  
   476  	hc := &container.HostConfig{
   477  		SecurityOpt: options.SecurityOpt,
   478  		Isolation:   options.Isolation,
   479  		ShmSize:     options.ShmSize,
   480  		Resources:   resources,
   481  		NetworkMode: container.NetworkMode(options.NetworkMode),
   482  		// Set a log config to override any default value set on the daemon
   483  		LogConfig:  defaultLogConfig,
   484  		ExtraHosts: options.ExtraHosts,
   485  	}
   486  	return hc
   487  }
   488  
   489  // fromSlash works like filepath.FromSlash but with a given OS platform field
   490  func fromSlash(path, platform string) string {
   491  	if platform == "windows" {
   492  		return strings.Replace(path, "/", "\\", -1)
   493  	}
   494  	return path
   495  }
   496  
   497  // separator returns a OS path separator for the given OS platform
   498  func separator(platform string) byte {
   499  	if platform == "windows" {
   500  		return '\\'
   501  	}
   502  	return '/'
   503  }