mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
ba40132366
Signed-off-by: John Howard <jhoward@microsoft.com>
361 lines
11 KiB
Go
361 lines
11 KiB
Go
package dockerfile
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/backend"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/builder"
|
|
"github.com/docker/docker/builder/dockerfile/command"
|
|
"github.com/docker/docker/builder/dockerfile/parser"
|
|
"github.com/docker/docker/builder/remotecontext"
|
|
"github.com/docker/docker/pkg/archive"
|
|
"github.com/docker/docker/pkg/chrootarchive"
|
|
"github.com/docker/docker/pkg/idtools"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
"github.com/docker/docker/pkg/system"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/sync/syncmap"
|
|
)
|
|
|
|
var validCommitCommands = map[string]bool{
|
|
"cmd": true,
|
|
"entrypoint": true,
|
|
"healthcheck": true,
|
|
"env": true,
|
|
"expose": true,
|
|
"label": true,
|
|
"onbuild": true,
|
|
"user": true,
|
|
"volume": true,
|
|
"workdir": true,
|
|
}
|
|
|
|
// BuildManager is shared across all Builder objects
|
|
type BuildManager struct {
|
|
archiver *archive.Archiver
|
|
backend builder.Backend
|
|
pathCache pathCache // TODO: make this persistent
|
|
}
|
|
|
|
// NewBuildManager creates a BuildManager
|
|
func NewBuildManager(b builder.Backend, idMappings *idtools.IDMappings) *BuildManager {
|
|
return &BuildManager{
|
|
backend: b,
|
|
pathCache: &syncmap.Map{},
|
|
archiver: chrootarchive.NewArchiver(idMappings),
|
|
}
|
|
}
|
|
|
|
// Build starts a new build from a BuildConfig
|
|
func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
|
|
buildsTriggered.Inc()
|
|
if config.Options.Dockerfile == "" {
|
|
config.Options.Dockerfile = builder.DefaultDockerfileName
|
|
}
|
|
|
|
source, dockerfile, err := remotecontext.Detect(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if source != nil {
|
|
defer func() {
|
|
if err := source.Close(); err != nil {
|
|
logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// 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"
|
|
}
|
|
}
|
|
|
|
builderOptions := builderOptions{
|
|
Options: config.Options,
|
|
ProgressWriter: config.ProgressWriter,
|
|
Backend: bm.backend,
|
|
PathCache: bm.pathCache,
|
|
Archiver: bm.archiver,
|
|
Platform: dockerfile.Platform,
|
|
}
|
|
|
|
return newBuilder(ctx, builderOptions).build(source, dockerfile)
|
|
}
|
|
|
|
// builderOptions are the dependencies required by the builder
|
|
type builderOptions struct {
|
|
Options *types.ImageBuildOptions
|
|
Backend builder.Backend
|
|
ProgressWriter backend.ProgressWriter
|
|
PathCache pathCache
|
|
Archiver *archive.Archiver
|
|
Platform string
|
|
}
|
|
|
|
// Builder is a Dockerfile builder
|
|
// It implements the builder.Backend interface.
|
|
type Builder struct {
|
|
options *types.ImageBuildOptions
|
|
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
Aux *streamformatter.AuxFormatter
|
|
Output io.Writer
|
|
|
|
docker builder.Backend
|
|
clientCtx context.Context
|
|
|
|
archiver *archive.Archiver
|
|
buildStages *buildStages
|
|
disableCommit bool
|
|
buildArgs *buildArgs
|
|
imageSources *imageSources
|
|
pathCache pathCache
|
|
containerManager *containerManager
|
|
imageProber ImageProber
|
|
|
|
// 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
|
|
}
|
|
|
|
// newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
|
|
// 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.
|
|
func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
|
|
config := options.Options
|
|
if config == nil {
|
|
config = new(types.ImageBuildOptions)
|
|
}
|
|
|
|
// @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"
|
|
}
|
|
|
|
b := &Builder{
|
|
clientCtx: clientCtx,
|
|
options: config,
|
|
Stdout: options.ProgressWriter.StdoutFormatter,
|
|
Stderr: options.ProgressWriter.StderrFormatter,
|
|
Aux: options.ProgressWriter.AuxFormatter,
|
|
Output: options.ProgressWriter.Output,
|
|
docker: options.Backend,
|
|
archiver: options.Archiver,
|
|
buildArgs: newBuildArgs(config.BuildArgs),
|
|
buildStages: newBuildStages(),
|
|
imageSources: newImageSources(clientCtx, options),
|
|
pathCache: options.PathCache,
|
|
imageProber: newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache),
|
|
containerManager: newContainerManager(options.Backend),
|
|
platform: options.Platform,
|
|
}
|
|
|
|
return b
|
|
}
|
|
|
|
// 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) {
|
|
defer b.imageSources.Unmount()
|
|
|
|
addNodesForLabelOption(dockerfile.AST, b.options.Labels)
|
|
|
|
if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
|
buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
|
|
return nil, err
|
|
}
|
|
|
|
dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
|
|
buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
|
|
return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
|
|
}
|
|
|
|
b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr)
|
|
|
|
if dispatchState.imageID == "" {
|
|
buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
|
|
return nil, errors.New("No image was generated. Is your Dockerfile empty?")
|
|
}
|
|
return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
|
|
}
|
|
|
|
func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
|
|
if aux == nil || state.imageID == "" {
|
|
return nil
|
|
}
|
|
return aux.Emit(types.BuildResult{ID: state.imageID})
|
|
}
|
|
|
|
func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
|
|
shlex := NewShellLex(dockerfile.EscapeToken)
|
|
state := newDispatchState()
|
|
total := len(dockerfile.AST.Children)
|
|
var err error
|
|
for i, n := range dockerfile.AST.Children {
|
|
select {
|
|
case <-b.clientCtx.Done():
|
|
logrus.Debug("Builder: build cancelled!")
|
|
fmt.Fprint(b.Stdout, "Build cancelled")
|
|
buildsFailed.WithValues(metricsBuildCanceled).Inc()
|
|
return nil, errors.New("Build cancelled")
|
|
default:
|
|
// Not cancelled yet, keep going...
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
if n.Value == command.From && state.isCurrentStage(b.options.Target) {
|
|
break
|
|
}
|
|
|
|
opts := dispatchOptions{
|
|
state: state,
|
|
stepMsg: formatStep(i, total),
|
|
node: n,
|
|
shlex: shlex,
|
|
source: source,
|
|
}
|
|
if state, err = b.dispatch(opts); err != nil {
|
|
if b.options.ForceRemove {
|
|
b.containerManager.RemoveAll(b.Stdout)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
|
|
if b.options.Remove {
|
|
b.containerManager.RemoveAll(b.Stdout)
|
|
}
|
|
}
|
|
|
|
// Emit a final aux message for the final image
|
|
if err := emitImageID(b.Aux, state); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return state, nil
|
|
}
|
|
|
|
func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
|
|
if len(labels) == 0 {
|
|
return
|
|
}
|
|
|
|
node := parser.NodeFromLabels(labels)
|
|
dockerfile.Children = append(dockerfile.Children, node)
|
|
}
|
|
|
|
// BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
|
|
// It will:
|
|
// - 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?
|
|
func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
|
|
if len(changes) == 0 {
|
|
return config, nil
|
|
}
|
|
|
|
b := newBuilder(context.Background(), builderOptions{
|
|
Options: &types.ImageBuildOptions{NoCache: true},
|
|
})
|
|
|
|
dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 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
|
|
|
|
// ensure that the commands are valid
|
|
for _, n := range dockerfile.AST.Children {
|
|
if !validCommitCommands[n.Value] {
|
|
return nil, fmt.Errorf("%s is not a valid change command", n.Value)
|
|
}
|
|
}
|
|
|
|
b.Stdout = ioutil.Discard
|
|
b.Stderr = ioutil.Discard
|
|
b.disableCommit = true
|
|
|
|
if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
|
|
return nil, err
|
|
}
|
|
dispatchState := newDispatchState()
|
|
dispatchState.runConfig = config
|
|
return dispatchFromDockerfile(b, dockerfile, dispatchState, nil)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
|
|
shlex := NewShellLex(result.EscapeToken)
|
|
ast := result.AST
|
|
total := len(ast.Children)
|
|
|
|
for i, n := range ast.Children {
|
|
opts := dispatchOptions{
|
|
state: dispatchState,
|
|
stepMsg: formatStep(i, total),
|
|
node: n,
|
|
shlex: shlex,
|
|
source: source,
|
|
}
|
|
if _, err := b.dispatch(opts); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return dispatchState.runConfig, nil
|
|
}
|