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