github.com/akerouanton/docker@v1.11.0-rc3/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  	"strings"
    11  	"sync"
    12  
    13  	"github.com/Sirupsen/logrus"
    14  	"github.com/docker/docker/builder"
    15  	"github.com/docker/docker/builder/dockerfile/parser"
    16  	"github.com/docker/docker/pkg/stringid"
    17  	"github.com/docker/docker/reference"
    18  	"github.com/docker/engine-api/types"
    19  	"github.com/docker/engine-api/types/container"
    20  	"golang.org/x/net/context"
    21  )
    22  
    23  var validCommitCommands = map[string]bool{
    24  	"cmd":        true,
    25  	"entrypoint": true,
    26  	"env":        true,
    27  	"expose":     true,
    28  	"label":      true,
    29  	"onbuild":    true,
    30  	"user":       true,
    31  	"volume":     true,
    32  	"workdir":    true,
    33  }
    34  
    35  // BuiltinAllowedBuildArgs is list of built-in allowed build args
    36  var BuiltinAllowedBuildArgs = map[string]bool{
    37  	"HTTP_PROXY":  true,
    38  	"http_proxy":  true,
    39  	"HTTPS_PROXY": true,
    40  	"https_proxy": true,
    41  	"FTP_PROXY":   true,
    42  	"ftp_proxy":   true,
    43  	"NO_PROXY":    true,
    44  	"no_proxy":    true,
    45  }
    46  
    47  // Builder is a Dockerfile builder
    48  // It implements the builder.Backend interface.
    49  type Builder struct {
    50  	options *types.ImageBuildOptions
    51  
    52  	Stdout io.Writer
    53  	Stderr io.Writer
    54  	Output io.Writer
    55  
    56  	docker    builder.Backend
    57  	context   builder.Context
    58  	clientCtx context.Context
    59  
    60  	dockerfile       *parser.Node
    61  	runConfig        *container.Config // runconfig for cmd, run, entrypoint etc.
    62  	flags            *BFlags
    63  	tmpContainers    map[string]struct{}
    64  	image            string // imageID
    65  	noBaseImage      bool
    66  	maintainer       string
    67  	cmdSet           bool
    68  	disableCommit    bool
    69  	cacheBusted      bool
    70  	cancelled        chan struct{}
    71  	cancelOnce       sync.Once
    72  	allowedBuildArgs map[string]bool // list of build-time args that are allowed for expansion/substitution and passing to commands in 'run'.
    73  
    74  	// TODO: remove once docker.Commit can receive a tag
    75  	id string
    76  }
    77  
    78  // BuildManager implements builder.Backend and is shared across all Builder objects.
    79  type BuildManager struct {
    80  	backend builder.Backend
    81  }
    82  
    83  // NewBuildManager creates a BuildManager.
    84  func NewBuildManager(b builder.Backend) (bm *BuildManager) {
    85  	return &BuildManager{backend: b}
    86  }
    87  
    88  // NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config.
    89  // If dockerfile is nil, the Dockerfile specified by Config.DockerfileName,
    90  // will be read from the Context passed to Build().
    91  func NewBuilder(clientCtx context.Context, config *types.ImageBuildOptions, backend builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
    92  	if config == nil {
    93  		config = new(types.ImageBuildOptions)
    94  	}
    95  	if config.BuildArgs == nil {
    96  		config.BuildArgs = make(map[string]string)
    97  	}
    98  	b = &Builder{
    99  		clientCtx:        clientCtx,
   100  		options:          config,
   101  		Stdout:           os.Stdout,
   102  		Stderr:           os.Stderr,
   103  		docker:           backend,
   104  		context:          context,
   105  		runConfig:        new(container.Config),
   106  		tmpContainers:    map[string]struct{}{},
   107  		cancelled:        make(chan struct{}),
   108  		id:               stringid.GenerateNonCryptoID(),
   109  		allowedBuildArgs: make(map[string]bool),
   110  	}
   111  	if dockerfile != nil {
   112  		b.dockerfile, err = parser.Parse(dockerfile)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  	}
   117  
   118  	return b, nil
   119  }
   120  
   121  // sanitizeRepoAndTags parses the raw "t" parameter received from the client
   122  // to a slice of repoAndTag.
   123  // It also validates each repoName and tag.
   124  func sanitizeRepoAndTags(names []string) ([]reference.Named, error) {
   125  	var (
   126  		repoAndTags []reference.Named
   127  		// This map is used for deduplicating the "-t" parameter.
   128  		uniqNames = make(map[string]struct{})
   129  	)
   130  	for _, repo := range names {
   131  		if repo == "" {
   132  			continue
   133  		}
   134  
   135  		ref, err := reference.ParseNamed(repo)
   136  		if err != nil {
   137  			return nil, err
   138  		}
   139  
   140  		ref = reference.WithDefaultTag(ref)
   141  
   142  		if _, isCanonical := ref.(reference.Canonical); isCanonical {
   143  			return nil, errors.New("build tag cannot contain a digest")
   144  		}
   145  
   146  		if _, isTagged := ref.(reference.NamedTagged); !isTagged {
   147  			ref, err = reference.WithTag(ref, reference.DefaultTag)
   148  			if err != nil {
   149  				return nil, err
   150  			}
   151  		}
   152  
   153  		nameWithTag := ref.String()
   154  
   155  		if _, exists := uniqNames[nameWithTag]; !exists {
   156  			uniqNames[nameWithTag] = struct{}{}
   157  			repoAndTags = append(repoAndTags, ref)
   158  		}
   159  	}
   160  	return repoAndTags, nil
   161  }
   162  
   163  // Build creates a NewBuilder, which builds the image.
   164  func (bm *BuildManager) Build(clientCtx context.Context, config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) {
   165  	b, err := NewBuilder(clientCtx, config, bm.backend, context, nil)
   166  	if err != nil {
   167  		return "", err
   168  	}
   169  	img, err := b.build(config, context, stdout, stderr, out, clientGone)
   170  	return img, err
   171  
   172  }
   173  
   174  // build runs the Dockerfile builder from a context and a docker object that allows to make calls
   175  // to Docker.
   176  //
   177  // This will (barring errors):
   178  //
   179  // * read the dockerfile from context
   180  // * parse the dockerfile if not already parsed
   181  // * walk the AST and execute it by dispatching to handlers. If Remove
   182  //   or ForceRemove is set, additional cleanup around containers happens after
   183  //   processing.
   184  // * Tag image, if applicable.
   185  // * Print a happy message and return the image ID.
   186  //
   187  func (b *Builder) build(config *types.ImageBuildOptions, context builder.Context, stdout io.Writer, stderr io.Writer, out io.Writer, clientGone <-chan bool) (string, error) {
   188  	b.options = config
   189  	b.context = context
   190  	b.Stdout = stdout
   191  	b.Stderr = stderr
   192  	b.Output = out
   193  
   194  	// If Dockerfile was not parsed yet, extract it from the Context
   195  	if b.dockerfile == nil {
   196  		if err := b.readDockerfile(); err != nil {
   197  			return "", err
   198  		}
   199  	}
   200  
   201  	finished := make(chan struct{})
   202  	defer close(finished)
   203  	go func() {
   204  		select {
   205  		case <-finished:
   206  		case <-clientGone:
   207  			b.cancelOnce.Do(func() {
   208  				close(b.cancelled)
   209  			})
   210  		}
   211  
   212  	}()
   213  
   214  	repoAndTags, err := sanitizeRepoAndTags(config.Tags)
   215  	if err != nil {
   216  		return "", err
   217  	}
   218  
   219  	var shortImgID string
   220  	for i, n := range b.dockerfile.Children {
   221  		// we only want to add labels to the last layer
   222  		if i == len(b.dockerfile.Children)-1 {
   223  			b.addLabels()
   224  		}
   225  		select {
   226  		case <-b.cancelled:
   227  			logrus.Debug("Builder: build cancelled!")
   228  			fmt.Fprintf(b.Stdout, "Build cancelled")
   229  			return "", fmt.Errorf("Build cancelled")
   230  		default:
   231  			// Not cancelled yet, keep going...
   232  		}
   233  		if err := b.dispatch(i, n); err != nil {
   234  			if b.options.ForceRemove {
   235  				b.clearTmp()
   236  			}
   237  			return "", err
   238  		}
   239  
   240  		// Commit the layer when there are only one children in
   241  		// the dockerfile, this is only the `FROM` tag, and
   242  		// build labels. Otherwise, the new image won't be
   243  		// labeled properly.
   244  		// Commit here, so the ID of the final image is reported
   245  		// properly.
   246  		if len(b.dockerfile.Children) == 1 && len(b.options.Labels) > 0 {
   247  			b.commit("", b.runConfig.Cmd, "")
   248  		}
   249  
   250  		shortImgID = stringid.TruncateID(b.image)
   251  		fmt.Fprintf(b.Stdout, " ---> %s\n", shortImgID)
   252  		if b.options.Remove {
   253  			b.clearTmp()
   254  		}
   255  	}
   256  
   257  	// check if there are any leftover build-args that were passed but not
   258  	// consumed during build. Return an error, if there are any.
   259  	leftoverArgs := []string{}
   260  	for arg := range b.options.BuildArgs {
   261  		if !b.isBuildArgAllowed(arg) {
   262  			leftoverArgs = append(leftoverArgs, arg)
   263  		}
   264  	}
   265  	if len(leftoverArgs) > 0 {
   266  		return "", fmt.Errorf("One or more build-args %v were not consumed, failing build.", leftoverArgs)
   267  	}
   268  
   269  	if b.image == "" {
   270  		return "", fmt.Errorf("No image was generated. Is your Dockerfile empty?")
   271  	}
   272  
   273  	for _, rt := range repoAndTags {
   274  		if err := b.docker.TagImage(rt, b.image); err != nil {
   275  			return "", err
   276  		}
   277  	}
   278  
   279  	fmt.Fprintf(b.Stdout, "Successfully built %s\n", shortImgID)
   280  	return b.image, nil
   281  }
   282  
   283  // Cancel cancels an ongoing Dockerfile build.
   284  func (b *Builder) Cancel() {
   285  	b.cancelOnce.Do(func() {
   286  		close(b.cancelled)
   287  	})
   288  }
   289  
   290  // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
   291  // It will:
   292  // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
   293  // - Do build by calling builder.dispatch() to call all entries' handling routines
   294  //
   295  // BuildFromConfig is used by the /commit endpoint, with the changes
   296  // coming from the query parameter of the same name.
   297  //
   298  // TODO: Remove?
   299  func BuildFromConfig(config *container.Config, changes []string) (*container.Config, error) {
   300  	ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  
   305  	// ensure that the commands are valid
   306  	for _, n := range ast.Children {
   307  		if !validCommitCommands[n.Value] {
   308  			return nil, fmt.Errorf("%s is not a valid change command", n.Value)
   309  		}
   310  	}
   311  
   312  	b, err := NewBuilder(context.Background(), nil, nil, nil, nil)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	b.runConfig = config
   317  	b.Stdout = ioutil.Discard
   318  	b.Stderr = ioutil.Discard
   319  	b.disableCommit = true
   320  
   321  	for i, n := range ast.Children {
   322  		if err := b.dispatch(i, n); err != nil {
   323  			return nil, err
   324  		}
   325  	}
   326  
   327  	return b.runConfig, nil
   328  }