github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/builder/dockerfile/builder.go (about)

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