github.com/fabiokung/docker@v0.11.2-0.20170222101415-4534dcd49497/builder/dockerfile/builder.go (about)

     1  package dockerfile
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/distribution/reference"
    15  	apierrors "github.com/docker/docker/api/errors"
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/backend"
    18  	"github.com/docker/docker/api/types/container"
    19  	"github.com/docker/docker/builder"
    20  	"github.com/docker/docker/builder/dockerfile/parser"
    21  	"github.com/docker/docker/image"
    22  	"github.com/docker/docker/pkg/stringid"
    23  	perrors "github.com/pkg/errors"
    24  	"golang.org/x/net/context"
    25  )
    26  
    27  var validCommitCommands = map[string]bool{
    28  	"cmd":         true,
    29  	"entrypoint":  true,
    30  	"healthcheck": true,
    31  	"env":         true,
    32  	"expose":      true,
    33  	"label":       true,
    34  	"onbuild":     true,
    35  	"user":        true,
    36  	"volume":      true,
    37  	"workdir":     true,
    38  }
    39  
    40  // BuiltinAllowedBuildArgs is list of built-in allowed build args
    41  var BuiltinAllowedBuildArgs = map[string]bool{
    42  	"HTTP_PROXY":  true,
    43  	"http_proxy":  true,
    44  	"HTTPS_PROXY": true,
    45  	"https_proxy": true,
    46  	"FTP_PROXY":   true,
    47  	"ftp_proxy":   true,
    48  	"NO_PROXY":    true,
    49  	"no_proxy":    true,
    50  }
    51  
    52  var defaultLogConfig = container.LogConfig{Type: "none"}
    53  
    54  // Builder is a Dockerfile builder
    55  // It implements the builder.Backend interface.
    56  type Builder struct {
    57  	options *types.ImageBuildOptions
    58  
    59  	Stdout io.Writer
    60  	Stderr io.Writer
    61  	Output io.Writer
    62  
    63  	docker    builder.Backend
    64  	context   builder.Context
    65  	clientCtx context.Context
    66  	cancel    context.CancelFunc
    67  
    68  	dockerfile       *parser.Node
    69  	runConfig        *container.Config // runconfig for cmd, run, entrypoint etc.
    70  	flags            *BFlags
    71  	tmpContainers    map[string]struct{}
    72  	image            string // imageID
    73  	noBaseImage      bool
    74  	maintainer       string
    75  	cmdSet           bool
    76  	disableCommit    bool
    77  	cacheBusted      bool
    78  	allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
    79  	directive        parser.Directive
    80  
    81  	// TODO: remove once docker.Commit can receive a tag
    82  	id string
    83  
    84  	imageCache builder.ImageCache
    85  	from       builder.Image
    86  }
    87  
    88  // BuildManager implements builder.Backend and is shared across all Builder objects.
    89  type BuildManager struct {
    90  	backend builder.Backend
    91  }
    92  
    93  // NewBuildManager creates a BuildManager.
    94  func NewBuildManager(b builder.Backend) (bm *BuildManager) {
    95  	return &BuildManager{backend: b}
    96  }
    97  
    98  // BuildFromContext builds a new image from a given context.
    99  func (bm *BuildManager) BuildFromContext(ctx context.Context, src io.ReadCloser, remote string, buildOptions *types.ImageBuildOptions, pg backend.ProgressWriter) (string, error) {
   100  	if buildOptions.Squash && !bm.backend.HasExperimental() {
   101  		return "", apierrors.NewBadRequestError(errors.New("squash is only supported with experimental mode"))
   102  	}
   103  	buildContext, dockerfileName, err := builder.DetectContextFromRemoteURL(src, remote, pg.ProgressReaderFunc)
   104  	if err != nil {
   105  		return "", err
   106  	}
   107  	defer func() {
   108  		if err := buildContext.Close(); err != nil {
   109  			logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err)
   110  		}
   111  	}()
   112  
   113  	if len(dockerfileName) > 0 {
   114  		buildOptions.Dockerfile = dockerfileName
   115  	}
   116  	b, err := NewBuilder(ctx, buildOptions, bm.backend, builder.DockerIgnoreContext{ModifiableContext: buildContext}, nil)
   117  	if err != nil {
   118  		return "", err
   119  	}
   120  	return b.build(pg.StdoutFormatter, pg.StderrFormatter, pg.Output)
   121  }
   122  
   123  // NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config.
   124  // If dockerfile is nil, the Dockerfile specified by Config.DockerfileName,
   125  // will be read from the Context passed to Build().
   126  func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, backend builder.Backend, buildContext builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
   127  	if config == nil {
   128  		config = new(types.ImageBuildOptions)
   129  	}
   130  	if config.BuildArgs == nil {
   131  		config.BuildArgs = make(map[string]*string)
   132  	}
   133  	ctx, cancel := context.WithCancel(clientCtx)
   134  	b = &Builder{
   135  		clientCtx:        ctx,
   136  		cancel:           cancel,
   137  		options:          config,
   138  		Stdout:           os.Stdout,
   139  		Stderr:           os.Stderr,
   140  		docker:           backend,
   141  		context:          buildContext,
   142  		runConfig:        new(container.Config),
   143  		tmpContainers:    map[string]struct{}{},
   144  		id:               stringid.GenerateNonCryptoID(),
   145  		allowedBuildArgs: make(map[string]bool),
   146  		directive: parser.Directive{
   147  			EscapeSeen:           false,
   148  			LookingForDirectives: true,
   149  		},
   150  	}
   151  	if icb, ok := backend.(builder.ImageCacheBuilder); ok {
   152  		b.imageCache = icb.MakeImageCache(config.CacheFrom)
   153  	}
   154  
   155  	parser.SetEscapeToken(parser.DefaultEscapeToken, &b.directive) // Assume the default token for escape
   156  
   157  	if dockerfile != nil {
   158  		b.dockerfile, err = parser.Parse(dockerfile, &b.directive)
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  	}
   163  
   164  	return b, nil
   165  }
   166  
   167  // sanitizeRepoAndTags parses the raw "t" parameter received from the client
   168  // to a slice of repoAndTag.
   169  // It also validates each repoName and tag.
   170  func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
   171  	var (
   172  		repoAndTags []reference.Named
   173  		// This map is used for deduplicating the "-t" parameter.
   174  		uniqNames = make(map[string]struct{})
   175  	)
   176  	for _, repo := range names {
   177  		if repo == "" {
   178  			continue
   179  		}
   180  
   181  		ref, err := reference.ParseNormalizedNamed(repo)
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  
   186  		if _, isCanonical := ref.(reference.Canonical); isCanonical {
   187  			return nil, errors.New("build tag cannot contain a digest")
   188  		}
   189  
   190  		ref = reference.TagNameOnly(ref)
   191  
   192  		nameWithTag := ref.String()
   193  
   194  		if _, exists := uniqNames[nameWithTag]; !exists {
   195  			uniqNames[nameWithTag] = struct{}{}
   196  			repoAndTags = append(repoAndTags, ref)
   197  		}
   198  	}
   199  	return repoAndTags, nil
   200  }
   201  
   202  func (b *Builder) processLabels() error {
   203  	if len(b.options.Labels) == 0 {
   204  		return nil
   205  	}
   206  
   207  	var labels []string
   208  	for k, v := range b.options.Labels {
   209  		labels = append(labels, fmt.Sprintf("%q='%s'", k, v))
   210  	}
   211  	// Sort the label to have a repeatable order
   212  	sort.Strings(labels)
   213  
   214  	line := "LABEL " + strings.Join(labels, " ")
   215  	_, node, err := parser.ParseLine(line, &b.directive, false)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	b.dockerfile.Children = append(b.dockerfile.Children, node)
   220  
   221  	return nil
   222  }
   223  
   224  // build runs the Dockerfile builder from a context and a docker object that allows to make calls
   225  // to Docker.
   226  //
   227  // This will (barring errors):
   228  //
   229  // * read the dockerfile from context
   230  // * parse the dockerfile if not already parsed
   231  // * walk the AST and execute it by dispatching to handlers. If Remove
   232  //   or ForceRemove is set, additional cleanup around containers happens after
   233  //   processing.
   234  // * Tag image, if applicable.
   235  // * Print a happy message and return the image ID.
   236  //
   237  func (b *Builder) build(stdout io.Writer, stderr io.Writer, out io.Writer) (string, error) {
   238  	b.Stdout = stdout
   239  	b.Stderr = stderr
   240  	b.Output = out
   241  
   242  	// If Dockerfile was not parsed yet, extract it from the Context
   243  	if b.dockerfile == nil {
   244  		if err := b.readDockerfile(); err != nil {
   245  			return "", err
   246  		}
   247  	}
   248  
   249  	repoAndTags, err := sanitizeRepoAndTags(b.options.Tags)
   250  	if err != nil {
   251  		return "", err
   252  	}
   253  
   254  	if err := b.processLabels(); err != nil {
   255  		return "", err
   256  	}
   257  
   258  	var shortImgID string
   259  	total := len(b.dockerfile.Children)
   260  	for _, n := range b.dockerfile.Children {
   261  		if err := b.checkDispatch(n, false); err != nil {
   262  			return "", perrors.Wrapf(err, "Dockerfile parse error line %d", n.StartLine)
   263  		}
   264  	}
   265  
   266  	for i, n := range b.dockerfile.Children {
   267  		select {
   268  		case <-b.clientCtx.Done():
   269  			logrus.Debug("Builder: build cancelled!")
   270  			fmt.Fprint(b.Stdout, "Build cancelled")
   271  			return "", errors.New("Build cancelled")
   272  		default:
   273  			// Not cancelled yet, keep going...
   274  		}
   275  
   276  		if err := b.dispatch(i, total, n); err != nil {
   277  			if b.options.ForceRemove {
   278  				b.clearTmp()
   279  			}
   280  			return "", err
   281  		}
   282  
   283  		shortImgID = stringid.TruncateID(b.image)
   284  		fmt.Fprintf(b.Stdout, " ---> %s\n", shortImgID)
   285  		if b.options.Remove {
   286  			b.clearTmp()
   287  		}
   288  	}
   289  
   290  	// check if there are any leftover build-args that were passed but not
   291  	// consumed during build. Return a warning, if there are any.
   292  	leftoverArgs := []string{}
   293  	for arg := range b.options.BuildArgs {
   294  		if !b.isBuildArgAllowed(arg) {
   295  			leftoverArgs = append(leftoverArgs, arg)
   296  		}
   297  	}
   298  
   299  	if len(leftoverArgs) > 0 {
   300  		fmt.Fprintf(b.Stderr, "[Warning] One or more build-args %v were not consumed\n", leftoverArgs)
   301  	}
   302  
   303  	if b.image == "" {
   304  		return "", errors.New("No image was generated. Is your Dockerfile empty?")
   305  	}
   306  
   307  	if b.options.Squash {
   308  		var fromID string
   309  		if b.from != nil {
   310  			fromID = b.from.ImageID()
   311  		}
   312  		b.image, err = b.docker.SquashImage(b.image, fromID)
   313  		if err != nil {
   314  			return "", perrors.Wrap(err, "error squashing image")
   315  		}
   316  	}
   317  
   318  	imageID := image.ID(b.image)
   319  	for _, rt := range repoAndTags {
   320  		if err := b.docker.TagImageWithReference(imageID, rt); err != nil {
   321  			return "", err
   322  		}
   323  	}
   324  
   325  	fmt.Fprintf(b.Stdout, "Successfully built %s\n", shortImgID)
   326  	return b.image, nil
   327  }
   328  
   329  // Cancel cancels an ongoing Dockerfile build.
   330  func (b *Builder) Cancel() {
   331  	b.cancel()
   332  }
   333  
   334  // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
   335  // It will:
   336  // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
   337  // - Do build by calling builder.dispatch() to call all entries' handling routines
   338  //
   339  // BuildFromConfig is used by the /commit endpoint, with the changes
   340  // coming from the query parameter of the same name.
   341  //
   342  // TODO: Remove?
   343  func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
   344  	b, err := NewBuilder(context.Background(), nil, nil, nil, nil)
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")), &b.directive)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  
   354  	// ensure that the commands are valid
   355  	for _, n := range ast.Children {
   356  		if !validCommitCommands[n.Value] {
   357  			return nil, fmt.Errorf("%s is not a valid change command", n.Value)
   358  		}
   359  	}
   360  
   361  	b.runConfig = config
   362  	b.Stdout = ioutil.Discard
   363  	b.Stderr = ioutil.Discard
   364  	b.disableCommit = true
   365  
   366  	total := len(ast.Children)
   367  	for _, n := range ast.Children {
   368  		if err := b.checkDispatch(n, false); err != nil {
   369  			return nil, err
   370  		}
   371  	}
   372  
   373  	for i, n := range ast.Children {
   374  		if err := b.dispatch(i, total, n); err != nil {
   375  			return nil, err
   376  		}
   377  	}
   378  
   379  	return b.runConfig, nil
   380  }