github.com/moby/docker@v26.1.3+incompatible/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  	"context"
     8  	"crypto/sha256"
     9  	"encoding/hex"
    10  	"fmt"
    11  	"strings"
    12  
    13  	"github.com/containerd/containerd/platforms"
    14  	"github.com/containerd/log"
    15  	"github.com/docker/docker/api/types"
    16  	"github.com/docker/docker/api/types/backend"
    17  	"github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/network"
    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/stringid"
    24  	"github.com/docker/docker/runconfig"
    25  	"github.com/docker/go-connections/nat"
    26  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  func (b *Builder) getArchiver() *archive.Archiver {
    31  	return chrootarchive.NewArchiver(b.idMapping)
    32  }
    33  
    34  func (b *Builder) commit(ctx context.Context, dispatchState *dispatchState, comment string) error {
    35  	if b.disableCommit {
    36  		return nil
    37  	}
    38  	if !dispatchState.hasFromImage() {
    39  		return errors.New("Please provide a source image with `from` prior to commit")
    40  	}
    41  
    42  	runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, dispatchState.operatingSystem))
    43  	id, err := b.probeAndCreate(ctx, dispatchState, runConfigWithCommentCmd)
    44  	if err != nil || id == "" {
    45  		return err
    46  	}
    47  
    48  	return b.commitContainer(ctx, dispatchState, id, runConfigWithCommentCmd)
    49  }
    50  
    51  func (b *Builder) commitContainer(ctx context.Context, dispatchState *dispatchState, id string, containerConfig *container.Config) error {
    52  	if b.disableCommit {
    53  		return nil
    54  	}
    55  
    56  	commitCfg := backend.CommitConfig{
    57  		Author: dispatchState.maintainer,
    58  		// TODO: this copy should be done by Commit()
    59  		Config:          copyRunConfig(dispatchState.runConfig),
    60  		ContainerConfig: containerConfig,
    61  		ContainerID:     id,
    62  	}
    63  
    64  	imageID, err := b.docker.CommitBuildStep(ctx, commitCfg)
    65  	dispatchState.imageID = string(imageID)
    66  	return err
    67  }
    68  
    69  func (b *Builder) exportImage(ctx context.Context, state *dispatchState, layer builder.RWLayer, parent builder.Image, runConfig *container.Config) error {
    70  	newLayer, err := layer.Commit()
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	parentImage, ok := parent.(*image.Image)
    76  	if !ok {
    77  		return errors.Errorf("unexpected image type")
    78  	}
    79  
    80  	platform := &ocispec.Platform{
    81  		OS:           parentImage.OS,
    82  		Architecture: parentImage.Architecture,
    83  		Variant:      parentImage.Variant,
    84  	}
    85  
    86  	// add an image mount without an image so the layer is properly unmounted
    87  	// if there is an error before we can add the full mount with image
    88  	b.imageSources.Add(newImageMount(nil, newLayer), platform)
    89  
    90  	newImage := image.NewChildImage(parentImage, image.ChildConfig{
    91  		Author:          state.maintainer,
    92  		ContainerConfig: runConfig,
    93  		DiffID:          newLayer.DiffID(),
    94  		Config:          copyRunConfig(state.runConfig),
    95  	}, parentImage.OS)
    96  
    97  	// TODO: it seems strange to marshal this here instead of just passing in the
    98  	// image struct
    99  	config, err := newImage.MarshalJSON()
   100  	if err != nil {
   101  		return errors.Wrap(err, "failed to encode image config")
   102  	}
   103  
   104  	// when writing the new image's manifest, we now need to pass in the new layer's digest.
   105  	// before the containerd store work this was unnecessary since we get the layer id
   106  	// from the image's RootFS ChainID -- see:
   107  	// https://github.com/moby/moby/blob/8cf66ed7322fa885ef99c4c044fa23e1727301dc/image/store.go#L162
   108  	// however, with the containerd store we can't do this. An alternative implementation here
   109  	// without changing the signature would be to get the layer digest by walking the content store
   110  	// and filtering the objects to find the layer with the DiffID we want, but that has performance
   111  	// implications that should be called out/investigated
   112  	exportedImage, err := b.docker.CreateImage(ctx, config, state.imageID, newLayer.ContentStoreDigest())
   113  	if err != nil {
   114  		return errors.Wrapf(err, "failed to export image")
   115  	}
   116  
   117  	state.imageID = exportedImage.ImageID()
   118  	b.imageSources.Add(newImageMount(exportedImage, newLayer), platform)
   119  	return nil
   120  }
   121  
   122  func (b *Builder) performCopy(ctx context.Context, req dispatchRequest, inst copyInstruction) error {
   123  	state := req.state
   124  	srcHash := getSourceHashFromInfos(inst.infos)
   125  
   126  	var chownComment string
   127  	if inst.chownStr != "" {
   128  		chownComment = fmt.Sprintf("--chown=%s ", inst.chownStr)
   129  	}
   130  	commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest)
   131  
   132  	// TODO: should this have been using origPaths instead of srcHash in the comment?
   133  	runConfigWithCommentCmd := copyRunConfig(
   134  		state.runConfig,
   135  		withCmdCommentString(commentStr, state.operatingSystem))
   136  	hit, err := b.probeCache(state, runConfigWithCommentCmd)
   137  	if err != nil || hit {
   138  		return err
   139  	}
   140  
   141  	imageMount, err := b.imageSources.Get(ctx, state.imageID, true, req.builder.platform)
   142  	if err != nil {
   143  		return errors.Wrapf(err, "failed to get destination image %q", state.imageID)
   144  	}
   145  
   146  	rwLayer, err := imageMount.NewRWLayer()
   147  	if err != nil {
   148  		return err
   149  	}
   150  	defer rwLayer.Release()
   151  
   152  	destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer, state.operatingSystem)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	identity := b.idMapping.RootPair()
   158  	// if a chown was requested, perform the steps to get the uid, gid
   159  	// translated (if necessary because of user namespaces), and replace
   160  	// the root pair with the chown pair for copy operations
   161  	if inst.chownStr != "" {
   162  		identity, err = parseChownFlag(ctx, b, state, inst.chownStr, destInfo.root, b.idMapping)
   163  		if err != nil {
   164  			if b.options.Platform != "windows" {
   165  				return errors.Wrapf(err, "unable to convert uid/gid chown string to host mapping")
   166  			}
   167  
   168  			return errors.Wrapf(err, "unable to map container user account name to SID")
   169  		}
   170  	}
   171  
   172  	for _, info := range inst.infos {
   173  		opts := copyFileOptions{
   174  			decompress: inst.allowLocalDecompression,
   175  			archiver:   b.getArchiver(),
   176  		}
   177  		if !inst.preserveOwnership {
   178  			opts.identity = &identity
   179  		}
   180  		if err := performCopyForInfo(destInfo, info, opts); err != nil {
   181  			return errors.Wrapf(err, "failed to copy files")
   182  		}
   183  	}
   184  	return b.exportImage(ctx, state, rwLayer, imageMount.Image(), runConfigWithCommentCmd)
   185  }
   186  
   187  func createDestInfo(workingDir string, inst copyInstruction, rwLayer builder.RWLayer, platform string) (copyInfo, error) {
   188  	// Twiddle the destination when it's a relative path - meaning, make it
   189  	// relative to the WORKINGDIR
   190  	dest, err := normalizeDest(workingDir, inst.dest)
   191  	if err != nil {
   192  		return copyInfo{}, errors.Wrapf(err, "invalid %s", inst.cmdName)
   193  	}
   194  
   195  	return copyInfo{root: rwLayer.Root(), path: dest}, nil
   196  }
   197  
   198  // For backwards compat, if there's just one info then use it as the
   199  // cache look-up string, otherwise hash 'em all into one
   200  func getSourceHashFromInfos(infos []copyInfo) string {
   201  	if len(infos) == 1 {
   202  		return infos[0].hash
   203  	}
   204  	var hashs []string
   205  	for _, info := range infos {
   206  		hashs = append(hashs, info.hash)
   207  	}
   208  	return hashStringSlice("multi", hashs)
   209  }
   210  
   211  func hashStringSlice(prefix string, slice []string) string {
   212  	hasher := sha256.New()
   213  	hasher.Write([]byte(strings.Join(slice, ",")))
   214  	return prefix + ":" + hex.EncodeToString(hasher.Sum(nil))
   215  }
   216  
   217  type runConfigModifier func(*container.Config)
   218  
   219  func withCmd(cmd []string) runConfigModifier {
   220  	return func(runConfig *container.Config) {
   221  		runConfig.Cmd = cmd
   222  	}
   223  }
   224  
   225  func withArgsEscaped(argsEscaped bool) runConfigModifier {
   226  	return func(runConfig *container.Config) {
   227  		runConfig.ArgsEscaped = argsEscaped
   228  	}
   229  }
   230  
   231  // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for
   232  // why there are two almost identical versions of this.
   233  func withCmdComment(comment string, platform string) runConfigModifier {
   234  	return func(runConfig *container.Config) {
   235  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment)
   236  	}
   237  }
   238  
   239  // withCmdCommentString exists to maintain compatibility with older versions.
   240  // A few instructions (workdir, copy, add) used a nop comment that is a single arg
   241  // where as all the other instructions used a two arg comment string. This
   242  // function implements the single arg version.
   243  func withCmdCommentString(comment string, platform string) runConfigModifier {
   244  	return func(runConfig *container.Config) {
   245  		runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment)
   246  	}
   247  }
   248  
   249  func withEnv(env []string) runConfigModifier {
   250  	return func(runConfig *container.Config) {
   251  		runConfig.Env = env
   252  	}
   253  }
   254  
   255  // withEntrypointOverride sets an entrypoint on runConfig if the command is
   256  // not empty. The entrypoint is left unmodified if command is empty.
   257  //
   258  // The dockerfile RUN instruction expect to run without an entrypoint
   259  // so the runConfig entrypoint needs to be modified accordingly. ContainerCreate
   260  // will change a []string{""} entrypoint to nil, so we probe the cache with the
   261  // nil entrypoint.
   262  func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier {
   263  	return func(runConfig *container.Config) {
   264  		if len(cmd) > 0 {
   265  			runConfig.Entrypoint = entrypoint
   266  		}
   267  	}
   268  }
   269  
   270  // withoutHealthcheck disables healthcheck.
   271  //
   272  // The dockerfile RUN instruction expect to run without healthcheck
   273  // so the runConfig Healthcheck needs to be disabled.
   274  func withoutHealthcheck() runConfigModifier {
   275  	return func(runConfig *container.Config) {
   276  		runConfig.Healthcheck = &container.HealthConfig{
   277  			Test: []string{"NONE"},
   278  		}
   279  	}
   280  }
   281  
   282  func copyRunConfig(runConfig *container.Config, modifiers ...runConfigModifier) *container.Config {
   283  	copy := *runConfig
   284  	copy.Cmd = copyStringSlice(runConfig.Cmd)
   285  	copy.Env = copyStringSlice(runConfig.Env)
   286  	copy.Entrypoint = copyStringSlice(runConfig.Entrypoint)
   287  	copy.OnBuild = copyStringSlice(runConfig.OnBuild)
   288  	copy.Shell = copyStringSlice(runConfig.Shell)
   289  
   290  	if copy.Volumes != nil {
   291  		copy.Volumes = make(map[string]struct{}, len(runConfig.Volumes))
   292  		for k, v := range runConfig.Volumes {
   293  			copy.Volumes[k] = v
   294  		}
   295  	}
   296  
   297  	if copy.ExposedPorts != nil {
   298  		copy.ExposedPorts = make(nat.PortSet, len(runConfig.ExposedPorts))
   299  		for k, v := range runConfig.ExposedPorts {
   300  			copy.ExposedPorts[k] = v
   301  		}
   302  	}
   303  
   304  	if copy.Labels != nil {
   305  		copy.Labels = make(map[string]string, len(runConfig.Labels))
   306  		for k, v := range runConfig.Labels {
   307  			copy.Labels[k] = v
   308  		}
   309  	}
   310  
   311  	for _, modifier := range modifiers {
   312  		modifier(&copy)
   313  	}
   314  	return &copy
   315  }
   316  
   317  func copyStringSlice(orig []string) []string {
   318  	if orig == nil {
   319  		return nil
   320  	}
   321  	return append([]string{}, orig...)
   322  }
   323  
   324  // getShell is a helper function which gets the right shell for prefixing the
   325  // shell-form of RUN, ENTRYPOINT and CMD instructions
   326  func getShell(c *container.Config, os string) []string {
   327  	if 0 == len(c.Shell) {
   328  		return append([]string{}, defaultShellForOS(os)[:]...)
   329  	}
   330  	return append([]string{}, c.Shell[:]...)
   331  }
   332  
   333  func (b *Builder) probeCache(dispatchState *dispatchState, runConfig *container.Config) (bool, error) {
   334  	cachedID, err := b.imageProber.Probe(dispatchState.imageID, runConfig, b.getPlatform(dispatchState))
   335  	if cachedID == "" || err != nil {
   336  		return false, err
   337  	}
   338  	fmt.Fprint(b.Stdout, " ---> Using cache\n")
   339  
   340  	dispatchState.imageID = cachedID
   341  	return true, nil
   342  }
   343  
   344  var defaultLogConfig = container.LogConfig{Type: "none"}
   345  
   346  func (b *Builder) probeAndCreate(ctx context.Context, dispatchState *dispatchState, runConfig *container.Config) (string, error) {
   347  	if hit, err := b.probeCache(dispatchState, runConfig); err != nil || hit {
   348  		return "", err
   349  	}
   350  	return b.create(ctx, runConfig)
   351  }
   352  
   353  func (b *Builder) create(ctx context.Context, runConfig *container.Config) (string, error) {
   354  	log.G(ctx).Debugf("[BUILDER] Command to be executed: %v", runConfig.Cmd)
   355  
   356  	hostConfig := hostConfigFromOptions(b.options)
   357  	container, err := b.containerManager.Create(ctx, runConfig, hostConfig)
   358  	if err != nil {
   359  		return "", err
   360  	}
   361  	// TODO: could this be moved into containerManager.Create() ?
   362  	for _, warning := range container.Warnings {
   363  		fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning)
   364  	}
   365  	fmt.Fprintf(b.Stdout, " ---> Running in %s\n", stringid.TruncateID(container.ID))
   366  	return container.ID, nil
   367  }
   368  
   369  func hostConfigFromOptions(options *types.ImageBuildOptions) *container.HostConfig {
   370  	resources := container.Resources{
   371  		CgroupParent: options.CgroupParent,
   372  		CPUShares:    options.CPUShares,
   373  		CPUPeriod:    options.CPUPeriod,
   374  		CPUQuota:     options.CPUQuota,
   375  		CpusetCpus:   options.CPUSetCPUs,
   376  		CpusetMems:   options.CPUSetMems,
   377  		Memory:       options.Memory,
   378  		MemorySwap:   options.MemorySwap,
   379  		Ulimits:      options.Ulimits,
   380  	}
   381  
   382  	// We need to make sure no empty string or "default" NetworkMode is
   383  	// provided to the daemon as it doesn't support them.
   384  	//
   385  	// This is in line with what the ContainerCreate API endpoint does.
   386  	networkMode := options.NetworkMode
   387  	if networkMode == "" || networkMode == network.NetworkDefault {
   388  		networkMode = runconfig.DefaultDaemonNetworkMode().NetworkName()
   389  	}
   390  
   391  	hc := &container.HostConfig{
   392  		SecurityOpt: options.SecurityOpt,
   393  		Isolation:   options.Isolation,
   394  		ShmSize:     options.ShmSize,
   395  		Resources:   resources,
   396  		NetworkMode: container.NetworkMode(networkMode),
   397  		// Set a log config to override any default value set on the daemon
   398  		LogConfig:  defaultLogConfig,
   399  		ExtraHosts: options.ExtraHosts,
   400  	}
   401  	return hc
   402  }
   403  
   404  func (b *Builder) getPlatform(state *dispatchState) ocispec.Platform {
   405  	// May be nil if not explicitly set in API/dockerfile
   406  	out := platforms.DefaultSpec()
   407  	if b.platform != nil {
   408  		out = *b.platform
   409  	}
   410  
   411  	if state.operatingSystem != "" {
   412  		out.OS = state.operatingSystem
   413  	}
   414  
   415  	return out
   416  }