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 |
} |