builder/dockerfile/builder.go
e0ef11a4
 package dockerfile
 
 import (
 	"bytes"
 	"fmt"
 	"io"
 	"io/ioutil"
08252bc9
 	"runtime"
e0ef11a4
 	"strings"
5c3d2d55
 	"time"
e0ef11a4
 
91e197d6
 	"github.com/docker/docker/api/types"
73ac6d19
 	"github.com/docker/docker/api/types/backend"
91e197d6
 	"github.com/docker/docker/api/types/container"
e0ef11a4
 	"github.com/docker/docker/builder"
33e07f41
 	"github.com/docker/docker/builder/dockerfile/command"
e0ef11a4
 	"github.com/docker/docker/builder/dockerfile/parser"
5c3d2d55
 	"github.com/docker/docker/builder/fscache"
d1faf3df
 	"github.com/docker/docker/builder/remotecontext"
51360965
 	"github.com/docker/docker/pkg/idtools"
5894bc1a
 	"github.com/docker/docker/pkg/streamformatter"
e0ef11a4
 	"github.com/docker/docker/pkg/stringid"
08252bc9
 	"github.com/docker/docker/pkg/system"
41445a47
 	"github.com/moby/buildkit/session"
c7fad9b7
 	"github.com/pkg/errors"
1009e6a4
 	"github.com/sirupsen/logrus"
c44e7a3e
 	"golang.org/x/net/context"
0296797f
 	"golang.org/x/sync/syncmap"
e0ef11a4
 )
 
 var validCommitCommands = map[string]bool{
b6c7becb
 	"cmd":         true,
 	"entrypoint":  true,
 	"healthcheck": true,
 	"env":         true,
 	"expose":      true,
 	"label":       true,
 	"onbuild":     true,
 	"user":        true,
 	"volume":      true,
 	"workdir":     true,
e0ef11a4
 }
 
ec7b6238
 // SessionGetter is object used to get access to a session by uuid
 type SessionGetter interface {
 	Get(ctx context.Context, uuid string) (session.Caller, error)
 }
 
0296797f
 // BuildManager is shared across all Builder objects
 type BuildManager struct {
7a7357da
 	idMappings *idtools.IDMappings
 	backend    builder.Backend
 	pathCache  pathCache // TODO: make this persistent
 	sg         SessionGetter
 	fsCache    *fscache.FSCache
0296797f
 }
 
 // NewBuildManager creates a BuildManager
5c3d2d55
 func NewBuildManager(b builder.Backend, sg SessionGetter, fsCache *fscache.FSCache, idMappings *idtools.IDMappings) (*BuildManager, error) {
 	bm := &BuildManager{
7a7357da
 		backend:    b,
 		pathCache:  &syncmap.Map{},
 		sg:         sg,
 		idMappings: idMappings,
 		fsCache:    fsCache,
b3bc7b28
 	}
5c3d2d55
 	if err := fsCache.RegisterTransport(remotecontext.ClientSessionRemote, NewClientSessionTransport()); err != nil {
 		return nil, err
 	}
 	return bm, nil
0296797f
 }
 
 // Build starts a new build from a BuildConfig
 func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
a28b173a
 	buildsTriggered.Inc()
0296797f
 	if config.Options.Dockerfile == "" {
 		config.Options.Dockerfile = builder.DefaultDockerfileName
 	}
 
 	source, dockerfile, err := remotecontext.Detect(config)
 	if err != nil {
 		return nil, err
 	}
5c3d2d55
 	defer func() {
 		if source != nil {
0296797f
 			if err := source.Close(); err != nil {
 				logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
 			}
5c3d2d55
 		}
 	}()
0296797f
 
08252bc9
 	// TODO @jhowardmsft LCOW support - this will require rework to allow both linux and Windows simultaneously.
 	// This is an interim solution to hardcode to linux if LCOW is turned on.
 	if dockerfile.Platform == "" {
 		dockerfile.Platform = runtime.GOOS
 		if dockerfile.Platform == "windows" && system.LCOWSupported() {
 			dockerfile.Platform = "linux"
 		}
 	}
 
ec7b6238
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
5c3d2d55
 	if src, err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil {
ec7b6238
 		return nil, err
5c3d2d55
 	} else if src != nil {
 		source = src
ec7b6238
 	}
 
0296797f
 	builderOptions := builderOptions{
 		Options:        config.Options,
 		ProgressWriter: config.ProgressWriter,
 		Backend:        bm.backend,
 		PathCache:      bm.pathCache,
7a7357da
 		IDMappings:     bm.idMappings,
08252bc9
 		Platform:       dockerfile.Platform,
0296797f
 	}
08252bc9
 
0296797f
 	return newBuilder(ctx, builderOptions).build(source, dockerfile)
 }
 
5c3d2d55
 func (bm *BuildManager) initializeClientSession(ctx context.Context, cancel func(), options *types.ImageBuildOptions) (builder.Source, error) {
ec7b6238
 	if options.SessionID == "" || bm.sg == nil {
5c3d2d55
 		return nil, nil
ec7b6238
 	}
 	logrus.Debug("client is session enabled")
5c3d2d55
 
 	ctx, cancelCtx := context.WithTimeout(ctx, sessionConnectTimeout)
 	defer cancelCtx()
 
ec7b6238
 	c, err := bm.sg.Get(ctx, options.SessionID)
 	if err != nil {
5c3d2d55
 		return nil, err
ec7b6238
 	}
 	go func() {
 		<-c.Context().Done()
 		cancel()
 	}()
5c3d2d55
 	if options.RemoteContext == remotecontext.ClientSessionRemote {
 		st := time.Now()
ad46348d
 		csi, err := NewClientSessionSourceIdentifier(ctx, bm.sg, options.SessionID)
5c3d2d55
 		if err != nil {
 			return nil, err
 		}
 		src, err := bm.fsCache.SyncFrom(ctx, csi)
 		if err != nil {
 			return nil, err
 		}
 		logrus.Debugf("sync-time: %v", time.Since(st))
 		return src, nil
 	}
 	return nil, nil
ec7b6238
 }
 
0296797f
 // builderOptions are the dependencies required by the builder
 type builderOptions struct {
 	Options        *types.ImageBuildOptions
 	Backend        builder.Backend
 	ProgressWriter backend.ProgressWriter
 	PathCache      pathCache
7a7357da
 	IDMappings     *idtools.IDMappings
08252bc9
 	Platform       string
0296797f
 }
 
e0ef11a4
 // Builder is a Dockerfile builder
f8dc044a
 // It implements the builder.Backend interface.
e0ef11a4
 type Builder struct {
5190794f
 	options *types.ImageBuildOptions
e0ef11a4
 
 	Stdout io.Writer
 	Stderr io.Writer
5894bc1a
 	Aux    *streamformatter.AuxFormatter
9c332b16
 	Output io.Writer
e0ef11a4
 
c44e7a3e
 	docker    builder.Backend
 	clientCtx context.Context
e0ef11a4
 
7a7357da
 	idMappings       *idtools.IDMappings
19f3b071
 	buildStages      *buildStages
 	disableCommit    bool
 	buildArgs        *buildArgs
 	imageSources     *imageSources
 	pathCache        pathCache
 	containerManager *containerManager
 	imageProber      ImageProber
08252bc9
 
 	// TODO @jhowardmft LCOW Support. This will be moved to options at a later
 	// stage, however that cannot be done now as it affects the public API
 	// if it were.
 	platform string
9c332b16
 }
 
0296797f
 // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
08252bc9
 // TODO @jhowardmsft LCOW support: Eventually platform can be moved into the builder
 // options, however, that would be an API change as it shares types.ImageBuildOptions.
0296797f
 func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
 	config := options.Options
e0ef11a4
 	if config == nil {
5190794f
 		config = new(types.ImageBuildOptions)
e0ef11a4
 	}
08252bc9
 
 	// @jhowardmsft LCOW Support. For the time being, this is interim. Eventually
 	// will be moved to types.ImageBuildOptions, but it can't for now as that would
 	// be an API change.
 	if options.Platform == "" {
 		options.Platform = runtime.GOOS
 	}
 	if options.Platform == "windows" && system.LCOWSupported() {
 		options.Platform = "linux"
 	}
 
0296797f
 	b := &Builder{
19f3b071
 		clientCtx:        clientCtx,
 		options:          config,
 		Stdout:           options.ProgressWriter.StdoutFormatter,
 		Stderr:           options.ProgressWriter.StderrFormatter,
 		Aux:              options.ProgressWriter.AuxFormatter,
 		Output:           options.ProgressWriter.Output,
 		docker:           options.Backend,
7a7357da
 		idMappings:       options.IDMappings,
19f3b071
 		buildArgs:        newBuildArgs(config.BuildArgs),
 		buildStages:      newBuildStages(),
 		imageSources:     newImageSources(clientCtx, options),
 		pathCache:        options.PathCache,
ba401323
 		imageProber:      newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache),
19f3b071
 		containerManager: newContainerManager(options.Backend),
08252bc9
 		platform:         options.Platform,
e0ef11a4
 	}
08252bc9
 
0296797f
 	return b
e0ef11a4
 }
 
0296797f
 // Build runs the Dockerfile builder by parsing the Dockerfile and executing
 // the instructions from the file.
 func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
6c28e8ed
 	defer b.imageSources.Unmount()
f95f5828
 
64c4c1c3
 	addNodesForLabelOption(dockerfile.AST, b.options.Labels)
5844736c
 
64c4c1c3
 	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
a28b173a
 		buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
ebcb7d6b
 		return nil, validationError{err}
bfcd9581
 	}
 
213ed02e
 	dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
bfcd9581
 	if err != nil {
0296797f
 		return nil, err
bfcd9581
 	}
 
2f0ebba0
 	if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
a28b173a
 		buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
2f0ebba0
 		return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
 	}
 
b47b375c
 	dockerfile.PrintWarnings(b.Stderr)
72cc81ee
 	b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr)
bfcd9581
 
2f0ebba0
 	if dispatchState.imageID == "" {
a28b173a
 		buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
0296797f
 		return nil, errors.New("No image was generated. Is your Dockerfile empty?")
bfcd9581
 	}
2f0ebba0
 	return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
bfcd9581
 }
 
5894bc1a
 func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
 	if aux == nil || state.imageID == "" {
 		return nil
 	}
 	return aux.Emit(types.BuildResult{ID: state.imageID})
 }
 
213ed02e
 func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
2414166e
 	shlex := NewShellLex(dockerfile.EscapeToken)
2f0ebba0
 	state := newDispatchState()
64c4c1c3
 	total := len(dockerfile.AST.Children)
2f0ebba0
 	var err error
64c4c1c3
 	for i, n := range dockerfile.AST.Children {
e0ef11a4
 		select {
f2401a0f
 		case <-b.clientCtx.Done():
e0ef11a4
 			logrus.Debug("Builder: build cancelled!")
514adcf4
 			fmt.Fprint(b.Stdout, "Build cancelled")
a28b173a
 			buildsFailed.WithValues(metricsBuildCanceled).Inc()
2f0ebba0
 			return nil, errors.New("Build cancelled")
e0ef11a4
 		default:
 			// Not cancelled yet, keep going...
 		}
c8dc2b15
 
5894bc1a
 		// If this is a FROM and we have a previous image then
 		// emit an aux message for that image since it is the
 		// end of the previous stage
 		if n.Value == command.From {
 			if err := emitImageID(b.Aux, state); err != nil {
 				return nil, err
 			}
 		}
 
2f0ebba0
 		if n.Value == command.From && state.isCurrentStage(b.options.Target) {
33e07f41
 			break
 		}
 
2f0ebba0
 		opts := dispatchOptions{
 			state:   state,
 			stepMsg: formatStep(i, total),
 			node:    n,
 			shlex:   shlex,
213ed02e
 			source:  source,
2f0ebba0
 		}
 		if state, err = b.dispatch(opts); err != nil {
5190794f
 			if b.options.ForceRemove {
19f3b071
 				b.containerManager.RemoveAll(b.Stdout)
e0ef11a4
 			}
2f0ebba0
 			return nil, err
e0ef11a4
 		}
1a85c8eb
 
2f0ebba0
 		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
5190794f
 		if b.options.Remove {
19f3b071
 			b.containerManager.RemoveAll(b.Stdout)
e0ef11a4
 		}
 	}
5894bc1a
 
 	// Emit a final aux message for the final image
 	if err := emitImageID(b.Aux, state); err != nil {
 		return nil, err
 	}
 
2f0ebba0
 	return state, nil
bfcd9581
 }
e0ef11a4
 
f3e205dd
 func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
 	if len(labels) == 0 {
 		return
 	}
 
 	node := parser.NodeFromLabels(labels)
 	dockerfile.Children = append(dockerfile.Children, node)
 }
 
9c09a79b
 // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
 // It will:
a4ce361a
 // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
 // - Do build by calling builder.dispatch() to call all entries' handling routines
 //
 // BuildFromConfig is used by the /commit endpoint, with the changes
 // coming from the query parameter of the same name.
 //
 // TODO: Remove?
7ac4232e
 func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
9f738cc5
 	if len(changes) == 0 {
 		return config, nil
 	}
 
19f3b071
 	b := newBuilder(context.Background(), builderOptions{
 		Options: &types.ImageBuildOptions{NoCache: true},
 	})
755be795
 
2f0ebba0
 	dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
e0ef11a4
 	if err != nil {
ebcb7d6b
 		return nil, validationError{err}
e0ef11a4
 	}
 
08252bc9
 	// TODO @jhowardmsft LCOW support. For now, if LCOW enabled, switch to linux.
 	// Also explicitly set the platform. Ultimately this will be in the builder
 	// options, but we can't do that yet as it would change the API.
 	if dockerfile.Platform == "" {
 		dockerfile.Platform = runtime.GOOS
 	}
 	if dockerfile.Platform == "windows" && system.LCOWSupported() {
 		dockerfile.Platform = "linux"
 	}
 	b.platform = dockerfile.Platform
 
e0ef11a4
 	// ensure that the commands are valid
2f0ebba0
 	for _, n := range dockerfile.AST.Children {
e0ef11a4
 		if !validCommitCommands[n.Value] {
ebcb7d6b
 			return nil, validationError{errors.Errorf("%s is not a valid change command", n.Value)}
e0ef11a4
 		}
 	}
 
 	b.Stdout = ioutil.Discard
 	b.Stderr = ioutil.Discard
 	b.disableCommit = true
 
2f0ebba0
 	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
ebcb7d6b
 		return nil, validationError{err}
bfcd9581
 	}
2f0ebba0
 	dispatchState := newDispatchState()
 	dispatchState.runConfig = config
3f260415
 	return dispatchFromDockerfile(b, dockerfile, dispatchState, nil)
bfcd9581
 }
 
 func checkDispatchDockerfile(dockerfile *parser.Node) error {
 	for _, n := range dockerfile.Children {
 		if err := checkDispatch(n); err != nil {
 			return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine)
c8dc2b15
 		}
 	}
bfcd9581
 	return nil
 }
c8dc2b15
 
3f260415
 func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
2414166e
 	shlex := NewShellLex(result.EscapeToken)
64c4c1c3
 	ast := result.AST
bfcd9581
 	total := len(ast.Children)
64c4c1c3
 
e0ef11a4
 	for i, n := range ast.Children {
2f0ebba0
 		opts := dispatchOptions{
 			state:   dispatchState,
 			stepMsg: formatStep(i, total),
 			node:    n,
 			shlex:   shlex,
3f260415
 			source:  source,
2f0ebba0
 		}
 		if _, err := b.dispatch(opts); err != nil {
 			return nil, err
e0ef11a4
 		}
 	}
2f0ebba0
 	return dispatchState.runConfig, nil
e0ef11a4
 }