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

     1  package dockerfile // import "github.com/docker/docker/builder/dockerfile"
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"sort"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/containerd/containerd/platforms"
    14  	"github.com/docker/docker/api/types"
    15  	"github.com/docker/docker/api/types/backend"
    16  	"github.com/docker/docker/api/types/container"
    17  	"github.com/docker/docker/builder"
    18  	"github.com/docker/docker/builder/fscache"
    19  	"github.com/docker/docker/builder/remotecontext"
    20  	"github.com/docker/docker/errdefs"
    21  	"github.com/docker/docker/pkg/idtools"
    22  	"github.com/docker/docker/pkg/streamformatter"
    23  	"github.com/docker/docker/pkg/stringid"
    24  	"github.com/docker/docker/pkg/system"
    25  	"github.com/moby/buildkit/frontend/dockerfile/instructions"
    26  	"github.com/moby/buildkit/frontend/dockerfile/parser"
    27  	"github.com/moby/buildkit/frontend/dockerfile/shell"
    28  	"github.com/moby/buildkit/session"
    29  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    30  	"github.com/pkg/errors"
    31  	"github.com/sirupsen/logrus"
    32  	"golang.org/x/sync/syncmap"
    33  )
    34  
    35  var validCommitCommands = map[string]bool{
    36  	"cmd":         true,
    37  	"entrypoint":  true,
    38  	"healthcheck": true,
    39  	"env":         true,
    40  	"expose":      true,
    41  	"label":       true,
    42  	"onbuild":     true,
    43  	"user":        true,
    44  	"volume":      true,
    45  	"workdir":     true,
    46  }
    47  
    48  const (
    49  	stepFormat = "Step %d/%d : %v"
    50  )
    51  
    52  // SessionGetter is object used to get access to a session by uuid
    53  type SessionGetter interface {
    54  	Get(ctx context.Context, uuid string) (session.Caller, error)
    55  }
    56  
    57  // BuildManager is shared across all Builder objects
    58  type BuildManager struct {
    59  	idMapping *idtools.IdentityMapping
    60  	backend   builder.Backend
    61  	pathCache pathCache // TODO: make this persistent
    62  	sg        SessionGetter
    63  	fsCache   *fscache.FSCache
    64  }
    65  
    66  // NewBuildManager creates a BuildManager
    67  func NewBuildManager(b builder.Backend, sg SessionGetter, fsCache *fscache.FSCache, identityMapping *idtools.IdentityMapping) (*BuildManager, error) {
    68  	bm := &BuildManager{
    69  		backend:   b,
    70  		pathCache: &syncmap.Map{},
    71  		sg:        sg,
    72  		idMapping: identityMapping,
    73  		fsCache:   fsCache,
    74  	}
    75  	if err := fsCache.RegisterTransport(remotecontext.ClientSessionRemote, NewClientSessionTransport()); err != nil {
    76  		return nil, err
    77  	}
    78  	return bm, nil
    79  }
    80  
    81  // Build starts a new build from a BuildConfig
    82  func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
    83  	buildsTriggered.Inc()
    84  	if config.Options.Dockerfile == "" {
    85  		config.Options.Dockerfile = builder.DefaultDockerfileName
    86  	}
    87  
    88  	source, dockerfile, err := remotecontext.Detect(config)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	defer func() {
    93  		if source != nil {
    94  			if err := source.Close(); err != nil {
    95  				logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
    96  			}
    97  		}
    98  	}()
    99  
   100  	ctx, cancel := context.WithCancel(ctx)
   101  	defer cancel()
   102  
   103  	if src, err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil {
   104  		return nil, err
   105  	} else if src != nil {
   106  		source = src
   107  	}
   108  
   109  	builderOptions := builderOptions{
   110  		Options:        config.Options,
   111  		ProgressWriter: config.ProgressWriter,
   112  		Backend:        bm.backend,
   113  		PathCache:      bm.pathCache,
   114  		IDMapping:      bm.idMapping,
   115  	}
   116  	b, err := newBuilder(ctx, builderOptions)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	return b.build(source, dockerfile)
   121  }
   122  
   123  func (bm *BuildManager) initializeClientSession(ctx context.Context, cancel func(), options *types.ImageBuildOptions) (builder.Source, error) {
   124  	if options.SessionID == "" || bm.sg == nil {
   125  		return nil, nil
   126  	}
   127  	logrus.Debug("client is session enabled")
   128  
   129  	connectCtx, cancelCtx := context.WithTimeout(ctx, sessionConnectTimeout)
   130  	defer cancelCtx()
   131  
   132  	c, err := bm.sg.Get(connectCtx, options.SessionID)
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	go func() {
   137  		<-c.Context().Done()
   138  		cancel()
   139  	}()
   140  	if options.RemoteContext == remotecontext.ClientSessionRemote {
   141  		st := time.Now()
   142  		csi, err := NewClientSessionSourceIdentifier(ctx, bm.sg, options.SessionID)
   143  		if err != nil {
   144  			return nil, err
   145  		}
   146  		src, err := bm.fsCache.SyncFrom(ctx, csi)
   147  		if err != nil {
   148  			return nil, err
   149  		}
   150  		logrus.Debugf("sync-time: %v", time.Since(st))
   151  		return src, nil
   152  	}
   153  	return nil, nil
   154  }
   155  
   156  // builderOptions are the dependencies required by the builder
   157  type builderOptions struct {
   158  	Options        *types.ImageBuildOptions
   159  	Backend        builder.Backend
   160  	ProgressWriter backend.ProgressWriter
   161  	PathCache      pathCache
   162  	IDMapping      *idtools.IdentityMapping
   163  }
   164  
   165  // Builder is a Dockerfile builder
   166  // It implements the builder.Backend interface.
   167  type Builder struct {
   168  	options *types.ImageBuildOptions
   169  
   170  	Stdout io.Writer
   171  	Stderr io.Writer
   172  	Aux    *streamformatter.AuxFormatter
   173  	Output io.Writer
   174  
   175  	docker    builder.Backend
   176  	clientCtx context.Context
   177  
   178  	idMapping        *idtools.IdentityMapping
   179  	disableCommit    bool
   180  	imageSources     *imageSources
   181  	pathCache        pathCache
   182  	containerManager *containerManager
   183  	imageProber      ImageProber
   184  	platform         *specs.Platform
   185  }
   186  
   187  // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
   188  func newBuilder(clientCtx context.Context, options builderOptions) (*Builder, error) {
   189  	config := options.Options
   190  	if config == nil {
   191  		config = new(types.ImageBuildOptions)
   192  	}
   193  
   194  	b := &Builder{
   195  		clientCtx:        clientCtx,
   196  		options:          config,
   197  		Stdout:           options.ProgressWriter.StdoutFormatter,
   198  		Stderr:           options.ProgressWriter.StderrFormatter,
   199  		Aux:              options.ProgressWriter.AuxFormatter,
   200  		Output:           options.ProgressWriter.Output,
   201  		docker:           options.Backend,
   202  		idMapping:        options.IDMapping,
   203  		imageSources:     newImageSources(clientCtx, options),
   204  		pathCache:        options.PathCache,
   205  		imageProber:      newImageProber(options.Backend, config.CacheFrom, config.NoCache),
   206  		containerManager: newContainerManager(options.Backend),
   207  	}
   208  
   209  	// same as in Builder.Build in builder/builder-next/builder.go
   210  	// TODO: remove once config.Platform is of type specs.Platform
   211  	if config.Platform != "" {
   212  		sp, err := platforms.Parse(config.Platform)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  		if err := system.ValidatePlatform(sp); err != nil {
   217  			return nil, err
   218  		}
   219  		b.platform = &sp
   220  	}
   221  
   222  	return b, nil
   223  }
   224  
   225  // Build 'LABEL' command(s) from '--label' options and add to the last stage
   226  func buildLabelOptions(labels map[string]string, stages []instructions.Stage) {
   227  	keys := []string{}
   228  	for key := range labels {
   229  		keys = append(keys, key)
   230  	}
   231  
   232  	// Sort the label to have a repeatable order
   233  	sort.Strings(keys)
   234  	for _, key := range keys {
   235  		value := labels[key]
   236  		stages[len(stages)-1].AddCommand(instructions.NewLabelCommand(key, value, true))
   237  	}
   238  }
   239  
   240  // Build runs the Dockerfile builder by parsing the Dockerfile and executing
   241  // the instructions from the file.
   242  func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
   243  	defer b.imageSources.Unmount()
   244  
   245  	stages, metaArgs, err := instructions.Parse(dockerfile.AST)
   246  	if err != nil {
   247  		if instructions.IsUnknownInstruction(err) {
   248  			buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
   249  		}
   250  		return nil, errdefs.InvalidParameter(err)
   251  	}
   252  	if b.options.Target != "" {
   253  		targetIx, found := instructions.HasStage(stages, b.options.Target)
   254  		if !found {
   255  			buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
   256  			return nil, errdefs.InvalidParameter(errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target))
   257  		}
   258  		stages = stages[:targetIx+1]
   259  	}
   260  
   261  	// Add 'LABEL' command specified by '--label' option to the last stage
   262  	buildLabelOptions(b.options.Labels, stages)
   263  
   264  	dockerfile.PrintWarnings(b.Stderr)
   265  	dispatchState, err := b.dispatchDockerfileWithCancellation(stages, metaArgs, dockerfile.EscapeToken, source)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	if dispatchState.imageID == "" {
   270  		buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
   271  		return nil, errors.New("No image was generated. Is your Dockerfile empty?")
   272  	}
   273  	return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
   274  }
   275  
   276  func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
   277  	if aux == nil || state.imageID == "" {
   278  		return nil
   279  	}
   280  	return aux.Emit("", types.BuildResult{ID: state.imageID})
   281  }
   282  
   283  func processMetaArg(meta instructions.ArgCommand, shlex *shell.Lex, args *BuildArgs) error {
   284  	// shell.Lex currently only support the concatenated string format
   285  	envs := convertMapToEnvList(args.GetAllAllowed())
   286  	if err := meta.Expand(func(word string) (string, error) {
   287  		return shlex.ProcessWord(word, envs)
   288  	}); err != nil {
   289  		return err
   290  	}
   291  	args.AddArg(meta.Key, meta.Value)
   292  	args.AddMetaArg(meta.Key, meta.Value)
   293  	return nil
   294  }
   295  
   296  func printCommand(out io.Writer, currentCommandIndex int, totalCommands int, cmd interface{}) int {
   297  	fmt.Fprintf(out, stepFormat, currentCommandIndex, totalCommands, cmd)
   298  	fmt.Fprintln(out)
   299  	return currentCommandIndex + 1
   300  }
   301  
   302  func (b *Builder) dispatchDockerfileWithCancellation(parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) {
   303  	dispatchRequest := dispatchRequest{}
   304  	buildArgs := NewBuildArgs(b.options.BuildArgs)
   305  	totalCommands := len(metaArgs) + len(parseResult)
   306  	currentCommandIndex := 1
   307  	for _, stage := range parseResult {
   308  		totalCommands += len(stage.Commands)
   309  	}
   310  	shlex := shell.NewLex(escapeToken)
   311  	for _, meta := range metaArgs {
   312  		currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &meta)
   313  
   314  		err := processMetaArg(meta, shlex, buildArgs)
   315  		if err != nil {
   316  			return nil, err
   317  		}
   318  	}
   319  
   320  	stagesResults := newStagesBuildResults()
   321  
   322  	for _, stage := range parseResult {
   323  		if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil {
   324  			return nil, err
   325  		}
   326  		dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults)
   327  
   328  		currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode)
   329  		if err := initializeStage(dispatchRequest, &stage); err != nil {
   330  			return nil, err
   331  		}
   332  		dispatchRequest.state.updateRunConfig()
   333  		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
   334  		for _, cmd := range stage.Commands {
   335  			select {
   336  			case <-b.clientCtx.Done():
   337  				logrus.Debug("Builder: build cancelled!")
   338  				fmt.Fprint(b.Stdout, "Build cancelled\n")
   339  				buildsFailed.WithValues(metricsBuildCanceled).Inc()
   340  				return nil, errors.New("Build cancelled")
   341  			default:
   342  				// Not cancelled yet, keep going...
   343  			}
   344  
   345  			currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd)
   346  
   347  			if err := dispatch(dispatchRequest, cmd); err != nil {
   348  				return nil, err
   349  			}
   350  			dispatchRequest.state.updateRunConfig()
   351  			fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
   352  
   353  		}
   354  		if err := emitImageID(b.Aux, dispatchRequest.state); err != nil {
   355  			return nil, err
   356  		}
   357  		buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs)
   358  		if err := commitStage(dispatchRequest.state, stagesResults); err != nil {
   359  			return nil, err
   360  		}
   361  	}
   362  	buildArgs.WarnOnUnusedBuildArgs(b.Stdout)
   363  	return dispatchRequest.state, nil
   364  }
   365  
   366  // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
   367  // It will:
   368  // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
   369  // - Do build by calling builder.dispatch() to call all entries' handling routines
   370  //
   371  // BuildFromConfig is used by the /commit endpoint, with the changes
   372  // coming from the query parameter of the same name.
   373  //
   374  // TODO: Remove?
   375  func BuildFromConfig(config *container.Config, changes []string, os string) (*container.Config, error) {
   376  	if !system.IsOSSupported(os) {
   377  		return nil, errdefs.InvalidParameter(system.ErrNotSupportedOperatingSystem)
   378  	}
   379  	if len(changes) == 0 {
   380  		return config, nil
   381  	}
   382  
   383  	dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
   384  	if err != nil {
   385  		return nil, errdefs.InvalidParameter(err)
   386  	}
   387  
   388  	b, err := newBuilder(context.Background(), builderOptions{
   389  		Options: &types.ImageBuildOptions{NoCache: true},
   390  	})
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  
   395  	// ensure that the commands are valid
   396  	for _, n := range dockerfile.AST.Children {
   397  		if !validCommitCommands[n.Value] {
   398  			return nil, errdefs.InvalidParameter(errors.Errorf("%s is not a valid change command", n.Value))
   399  		}
   400  	}
   401  
   402  	b.Stdout = ioutil.Discard
   403  	b.Stderr = ioutil.Discard
   404  	b.disableCommit = true
   405  
   406  	var commands []instructions.Command
   407  	for _, n := range dockerfile.AST.Children {
   408  		cmd, err := instructions.ParseCommand(n)
   409  		if err != nil {
   410  			return nil, errdefs.InvalidParameter(err)
   411  		}
   412  		commands = append(commands, cmd)
   413  	}
   414  
   415  	dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, NewBuildArgs(b.options.BuildArgs), newStagesBuildResults())
   416  	// We make mutations to the configuration, ensure we have a copy
   417  	dispatchRequest.state.runConfig = copyRunConfig(config)
   418  	dispatchRequest.state.imageID = config.Image
   419  	dispatchRequest.state.operatingSystem = os
   420  	for _, cmd := range commands {
   421  		err := dispatch(dispatchRequest, cmd)
   422  		if err != nil {
   423  			return nil, errdefs.InvalidParameter(err)
   424  		}
   425  		dispatchRequest.state.updateRunConfig()
   426  	}
   427  
   428  	return dispatchRequest.state.runConfig, nil
   429  }
   430  
   431  func convertMapToEnvList(m map[string]string) []string {
   432  	result := []string{}
   433  	for k, v := range m {
   434  		result = append(result, k+"="+v)
   435  	}
   436  	return result
   437  }