github.com/jiasir/docker@v1.3.3-0.20170609024000-252e610103e7/builder/dockerfile/builder.go (about)

     1  package dockerfile
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"strings"
     9  
    10  	"github.com/Sirupsen/logrus"
    11  	"github.com/docker/docker/api/types"
    12  	"github.com/docker/docker/api/types/backend"
    13  	"github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/builder"
    15  	"github.com/docker/docker/builder/dockerfile/command"
    16  	"github.com/docker/docker/builder/dockerfile/parser"
    17  	"github.com/docker/docker/builder/remotecontext"
    18  	"github.com/docker/docker/pkg/streamformatter"
    19  	"github.com/docker/docker/pkg/stringid"
    20  	"github.com/pkg/errors"
    21  	"golang.org/x/net/context"
    22  	"golang.org/x/sync/syncmap"
    23  )
    24  
    25  var validCommitCommands = map[string]bool{
    26  	"cmd":         true,
    27  	"entrypoint":  true,
    28  	"healthcheck": true,
    29  	"env":         true,
    30  	"expose":      true,
    31  	"label":       true,
    32  	"onbuild":     true,
    33  	"user":        true,
    34  	"volume":      true,
    35  	"workdir":     true,
    36  }
    37  
    38  // BuildManager is shared across all Builder objects
    39  type BuildManager struct {
    40  	backend   builder.Backend
    41  	pathCache pathCache // TODO: make this persistent
    42  }
    43  
    44  // NewBuildManager creates a BuildManager
    45  func NewBuildManager(b builder.Backend) *BuildManager {
    46  	return &BuildManager{
    47  		backend:   b,
    48  		pathCache: &syncmap.Map{},
    49  	}
    50  }
    51  
    52  // Build starts a new build from a BuildConfig
    53  func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
    54  	buildsTriggered.Inc()
    55  	if config.Options.Dockerfile == "" {
    56  		config.Options.Dockerfile = builder.DefaultDockerfileName
    57  	}
    58  
    59  	source, dockerfile, err := remotecontext.Detect(config)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	if source != nil {
    64  		defer func() {
    65  			if err := source.Close(); err != nil {
    66  				logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
    67  			}
    68  		}()
    69  	}
    70  
    71  	builderOptions := builderOptions{
    72  		Options:        config.Options,
    73  		ProgressWriter: config.ProgressWriter,
    74  		Backend:        bm.backend,
    75  		PathCache:      bm.pathCache,
    76  	}
    77  	return newBuilder(ctx, builderOptions).build(source, dockerfile)
    78  }
    79  
    80  // builderOptions are the dependencies required by the builder
    81  type builderOptions struct {
    82  	Options        *types.ImageBuildOptions
    83  	Backend        builder.Backend
    84  	ProgressWriter backend.ProgressWriter
    85  	PathCache      pathCache
    86  }
    87  
    88  // Builder is a Dockerfile builder
    89  // It implements the builder.Backend interface.
    90  type Builder struct {
    91  	options *types.ImageBuildOptions
    92  
    93  	Stdout io.Writer
    94  	Stderr io.Writer
    95  	Aux    *streamformatter.AuxFormatter
    96  	Output io.Writer
    97  
    98  	docker    builder.Backend
    99  	clientCtx context.Context
   100  
   101  	buildStages      *buildStages
   102  	disableCommit    bool
   103  	buildArgs        *buildArgs
   104  	imageSources     *imageSources
   105  	pathCache        pathCache
   106  	containerManager *containerManager
   107  	imageProber      ImageProber
   108  }
   109  
   110  // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
   111  func newBuilder(clientCtx context.Context, options builderOptions) *Builder {
   112  	config := options.Options
   113  	if config == nil {
   114  		config = new(types.ImageBuildOptions)
   115  	}
   116  	b := &Builder{
   117  		clientCtx:        clientCtx,
   118  		options:          config,
   119  		Stdout:           options.ProgressWriter.StdoutFormatter,
   120  		Stderr:           options.ProgressWriter.StderrFormatter,
   121  		Aux:              options.ProgressWriter.AuxFormatter,
   122  		Output:           options.ProgressWriter.Output,
   123  		docker:           options.Backend,
   124  		buildArgs:        newBuildArgs(config.BuildArgs),
   125  		buildStages:      newBuildStages(),
   126  		imageSources:     newImageSources(clientCtx, options),
   127  		pathCache:        options.PathCache,
   128  		imageProber:      newImageProber(options.Backend, config.CacheFrom, config.NoCache),
   129  		containerManager: newContainerManager(options.Backend),
   130  	}
   131  	return b
   132  }
   133  
   134  // Build runs the Dockerfile builder by parsing the Dockerfile and executing
   135  // the instructions from the file.
   136  func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
   137  	defer b.imageSources.Unmount()
   138  
   139  	addNodesForLabelOption(dockerfile.AST, b.options.Labels)
   140  
   141  	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
   142  		buildsFailed.WithValues(metricsDockerfileSyntaxError).Inc()
   143  		return nil, err
   144  	}
   145  
   146  	dispatchState, err := b.dispatchDockerfileWithCancellation(dockerfile, source)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	if b.options.Target != "" && !dispatchState.isCurrentStage(b.options.Target) {
   152  		buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
   153  		return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target)
   154  	}
   155  
   156  	b.buildArgs.WarnOnUnusedBuildArgs(b.Stderr)
   157  
   158  	if dispatchState.imageID == "" {
   159  		buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
   160  		return nil, errors.New("No image was generated. Is your Dockerfile empty?")
   161  	}
   162  	return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
   163  }
   164  
   165  func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
   166  	if aux == nil || state.imageID == "" {
   167  		return nil
   168  	}
   169  	return aux.Emit(types.BuildResult{ID: state.imageID})
   170  }
   171  
   172  func (b *Builder) dispatchDockerfileWithCancellation(dockerfile *parser.Result, source builder.Source) (*dispatchState, error) {
   173  	shlex := NewShellLex(dockerfile.EscapeToken)
   174  	state := newDispatchState()
   175  	total := len(dockerfile.AST.Children)
   176  	var err error
   177  	for i, n := range dockerfile.AST.Children {
   178  		select {
   179  		case <-b.clientCtx.Done():
   180  			logrus.Debug("Builder: build cancelled!")
   181  			fmt.Fprint(b.Stdout, "Build cancelled")
   182  			buildsFailed.WithValues(metricsBuildCanceled).Inc()
   183  			return nil, errors.New("Build cancelled")
   184  		default:
   185  			// Not cancelled yet, keep going...
   186  		}
   187  
   188  		// If this is a FROM and we have a previous image then
   189  		// emit an aux message for that image since it is the
   190  		// end of the previous stage
   191  		if n.Value == command.From {
   192  			if err := emitImageID(b.Aux, state); err != nil {
   193  				return nil, err
   194  			}
   195  		}
   196  
   197  		if n.Value == command.From && state.isCurrentStage(b.options.Target) {
   198  			break
   199  		}
   200  
   201  		opts := dispatchOptions{
   202  			state:   state,
   203  			stepMsg: formatStep(i, total),
   204  			node:    n,
   205  			shlex:   shlex,
   206  			source:  source,
   207  		}
   208  		if state, err = b.dispatch(opts); err != nil {
   209  			if b.options.ForceRemove {
   210  				b.containerManager.RemoveAll(b.Stdout)
   211  			}
   212  			return nil, err
   213  		}
   214  
   215  		fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(state.imageID))
   216  		if b.options.Remove {
   217  			b.containerManager.RemoveAll(b.Stdout)
   218  		}
   219  	}
   220  
   221  	// Emit a final aux message for the final image
   222  	if err := emitImageID(b.Aux, state); err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	return state, nil
   227  }
   228  
   229  func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) {
   230  	if len(labels) == 0 {
   231  		return
   232  	}
   233  
   234  	node := parser.NodeFromLabels(labels)
   235  	dockerfile.Children = append(dockerfile.Children, node)
   236  }
   237  
   238  // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
   239  // It will:
   240  // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
   241  // - Do build by calling builder.dispatch() to call all entries' handling routines
   242  //
   243  // BuildFromConfig is used by the /commit endpoint, with the changes
   244  // coming from the query parameter of the same name.
   245  //
   246  // TODO: Remove?
   247  func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
   248  	if len(changes) == 0 {
   249  		return config, nil
   250  	}
   251  
   252  	b := newBuilder(context.Background(), builderOptions{
   253  		Options: &types.ImageBuildOptions{NoCache: true},
   254  	})
   255  
   256  	dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  
   261  	// ensure that the commands are valid
   262  	for _, n := range dockerfile.AST.Children {
   263  		if !validCommitCommands[n.Value] {
   264  			return nil, fmt.Errorf("%s is not a valid change command", n.Value)
   265  		}
   266  	}
   267  
   268  	b.Stdout = ioutil.Discard
   269  	b.Stderr = ioutil.Discard
   270  	b.disableCommit = true
   271  
   272  	if err := checkDispatchDockerfile(dockerfile.AST); err != nil {
   273  		return nil, err
   274  	}
   275  	dispatchState := newDispatchState()
   276  	dispatchState.runConfig = config
   277  	return dispatchFromDockerfile(b, dockerfile, dispatchState, nil)
   278  }
   279  
   280  func checkDispatchDockerfile(dockerfile *parser.Node) error {
   281  	for _, n := range dockerfile.Children {
   282  		if err := checkDispatch(n); err != nil {
   283  			return errors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine)
   284  		}
   285  	}
   286  	return nil
   287  }
   288  
   289  func dispatchFromDockerfile(b *Builder, result *parser.Result, dispatchState *dispatchState, source builder.Source) (*container.Config, error) {
   290  	shlex := NewShellLex(result.EscapeToken)
   291  	ast := result.AST
   292  	total := len(ast.Children)
   293  
   294  	for i, n := range ast.Children {
   295  		opts := dispatchOptions{
   296  			state:   dispatchState,
   297  			stepMsg: formatStep(i, total),
   298  			node:    n,
   299  			shlex:   shlex,
   300  			source:  source,
   301  		}
   302  		if _, err := b.dispatch(opts); err != nil {
   303  			return nil, err
   304  		}
   305  	}
   306  	return dispatchState.runConfig, nil
   307  }