github.com/mheon/docker@v0.11.2-0.20150922122814-44f47903a831/builder/job.go (about)

     1  package builder
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"runtime"
    11  	"strings"
    12  	"sync"
    13  
    14  	"github.com/docker/docker/api"
    15  	"github.com/docker/docker/builder/parser"
    16  	"github.com/docker/docker/cliconfig"
    17  	"github.com/docker/docker/daemon"
    18  	"github.com/docker/docker/graph/tags"
    19  	"github.com/docker/docker/pkg/archive"
    20  	"github.com/docker/docker/pkg/httputils"
    21  	"github.com/docker/docker/pkg/parsers"
    22  	"github.com/docker/docker/pkg/progressreader"
    23  	"github.com/docker/docker/pkg/streamformatter"
    24  	"github.com/docker/docker/pkg/stringid"
    25  	"github.com/docker/docker/pkg/ulimit"
    26  	"github.com/docker/docker/pkg/urlutil"
    27  	"github.com/docker/docker/registry"
    28  	"github.com/docker/docker/runconfig"
    29  	"github.com/docker/docker/utils"
    30  )
    31  
    32  // When downloading remote contexts, limit the amount (in bytes)
    33  // to be read from the response body in order to detect its Content-Type
    34  const maxPreambleLength = 100
    35  
    36  // whitelist of commands allowed for a commit/import
    37  var validCommitCommands = map[string]bool{
    38  	"cmd":        true,
    39  	"entrypoint": true,
    40  	"env":        true,
    41  	"expose":     true,
    42  	"label":      true,
    43  	"onbuild":    true,
    44  	"user":       true,
    45  	"volume":     true,
    46  	"workdir":    true,
    47  }
    48  
    49  // BuiltinAllowedBuildArgs is list of built-in allowed build args
    50  var BuiltinAllowedBuildArgs = map[string]bool{
    51  	"HTTP_PROXY":  true,
    52  	"http_proxy":  true,
    53  	"HTTPS_PROXY": true,
    54  	"https_proxy": true,
    55  	"FTP_PROXY":   true,
    56  	"ftp_proxy":   true,
    57  	"NO_PROXY":    true,
    58  	"no_proxy":    true,
    59  }
    60  
    61  // Config contains all configs for a build job
    62  type Config struct {
    63  	DockerfileName string
    64  	RemoteURL      string
    65  	RepoName       string
    66  	SuppressOutput bool
    67  	NoCache        bool
    68  	Remove         bool
    69  	ForceRemove    bool
    70  	Pull           bool
    71  	Memory         int64
    72  	MemorySwap     int64
    73  	CPUShares      int64
    74  	CPUPeriod      int64
    75  	CPUQuota       int64
    76  	CPUSetCpus     string
    77  	CPUSetMems     string
    78  	CgroupParent   string
    79  	Ulimits        []*ulimit.Ulimit
    80  	AuthConfigs    map[string]cliconfig.AuthConfig
    81  	BuildArgs      map[string]string
    82  
    83  	Stdout  io.Writer
    84  	Context io.ReadCloser
    85  	// When closed, the job has been cancelled.
    86  	// Note: not all jobs implement cancellation.
    87  	// See Job.Cancel() and Job.WaitCancelled()
    88  	cancelled  chan struct{}
    89  	cancelOnce sync.Once
    90  }
    91  
    92  // Cancel signals the build job to cancel
    93  func (b *Config) Cancel() {
    94  	b.cancelOnce.Do(func() {
    95  		close(b.cancelled)
    96  	})
    97  }
    98  
    99  // WaitCancelled returns a channel which is closed ("never blocks") when
   100  // the job is cancelled.
   101  func (b *Config) WaitCancelled() <-chan struct{} {
   102  	return b.cancelled
   103  }
   104  
   105  // NewBuildConfig returns a new Config struct
   106  func NewBuildConfig() *Config {
   107  	return &Config{
   108  		AuthConfigs: map[string]cliconfig.AuthConfig{},
   109  		cancelled:   make(chan struct{}),
   110  	}
   111  }
   112  
   113  // Build is the main interface of the package, it gathers the Builder
   114  // struct and calls builder.Run() to do all the real build job.
   115  func Build(d *daemon.Daemon, buildConfig *Config) error {
   116  	var (
   117  		repoName string
   118  		tag      string
   119  		context  io.ReadCloser
   120  	)
   121  	sf := streamformatter.NewJSONStreamFormatter()
   122  
   123  	repoName, tag = parsers.ParseRepositoryTag(buildConfig.RepoName)
   124  	if repoName != "" {
   125  		if err := registry.ValidateRepositoryName(repoName); err != nil {
   126  			return err
   127  		}
   128  		if len(tag) > 0 {
   129  			if err := tags.ValidateTagName(tag); err != nil {
   130  				return err
   131  			}
   132  		}
   133  	}
   134  
   135  	if buildConfig.RemoteURL == "" {
   136  		context = ioutil.NopCloser(buildConfig.Context)
   137  	} else if urlutil.IsGitURL(buildConfig.RemoteURL) {
   138  		root, err := utils.GitClone(buildConfig.RemoteURL)
   139  		if err != nil {
   140  			return err
   141  		}
   142  		defer os.RemoveAll(root)
   143  
   144  		c, err := archive.Tar(root, archive.Uncompressed)
   145  		if err != nil {
   146  			return err
   147  		}
   148  		context = c
   149  	} else if urlutil.IsURL(buildConfig.RemoteURL) {
   150  		f, err := httputils.Download(buildConfig.RemoteURL)
   151  		if err != nil {
   152  			return fmt.Errorf("Error downloading remote context %s: %v", buildConfig.RemoteURL, err)
   153  		}
   154  		defer f.Body.Close()
   155  		ct := f.Header.Get("Content-Type")
   156  		clen := f.ContentLength
   157  		contentType, bodyReader, err := inspectResponse(ct, f.Body, clen)
   158  
   159  		defer bodyReader.Close()
   160  
   161  		if err != nil {
   162  			return fmt.Errorf("Error detecting content type for remote %s: %v", buildConfig.RemoteURL, err)
   163  		}
   164  		if contentType == httputils.MimeTypes.TextPlain {
   165  			dockerFile, err := ioutil.ReadAll(bodyReader)
   166  			if err != nil {
   167  				return err
   168  			}
   169  
   170  			// When we're downloading just a Dockerfile put it in
   171  			// the default name - don't allow the client to move/specify it
   172  			buildConfig.DockerfileName = api.DefaultDockerfileName
   173  
   174  			c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile))
   175  			if err != nil {
   176  				return err
   177  			}
   178  			context = c
   179  		} else {
   180  			// Pass through - this is a pre-packaged context, presumably
   181  			// with a Dockerfile with the right name inside it.
   182  			prCfg := progressreader.Config{
   183  				In:        bodyReader,
   184  				Out:       buildConfig.Stdout,
   185  				Formatter: sf,
   186  				Size:      clen,
   187  				NewLines:  true,
   188  				ID:        "Downloading context",
   189  				Action:    buildConfig.RemoteURL,
   190  			}
   191  			context = progressreader.New(prCfg)
   192  		}
   193  	}
   194  
   195  	defer context.Close()
   196  
   197  	builder := &builder{
   198  		Daemon: d,
   199  		OutStream: &streamformatter.StdoutFormatter{
   200  			Writer:          buildConfig.Stdout,
   201  			StreamFormatter: sf,
   202  		},
   203  		ErrStream: &streamformatter.StderrFormatter{
   204  			Writer:          buildConfig.Stdout,
   205  			StreamFormatter: sf,
   206  		},
   207  		Verbose:          !buildConfig.SuppressOutput,
   208  		UtilizeCache:     !buildConfig.NoCache,
   209  		Remove:           buildConfig.Remove,
   210  		ForceRemove:      buildConfig.ForceRemove,
   211  		Pull:             buildConfig.Pull,
   212  		OutOld:           buildConfig.Stdout,
   213  		StreamFormatter:  sf,
   214  		AuthConfigs:      buildConfig.AuthConfigs,
   215  		dockerfileName:   buildConfig.DockerfileName,
   216  		cpuShares:        buildConfig.CPUShares,
   217  		cpuPeriod:        buildConfig.CPUPeriod,
   218  		cpuQuota:         buildConfig.CPUQuota,
   219  		cpuSetCpus:       buildConfig.CPUSetCpus,
   220  		cpuSetMems:       buildConfig.CPUSetMems,
   221  		cgroupParent:     buildConfig.CgroupParent,
   222  		memory:           buildConfig.Memory,
   223  		memorySwap:       buildConfig.MemorySwap,
   224  		ulimits:          buildConfig.Ulimits,
   225  		cancelled:        buildConfig.WaitCancelled(),
   226  		id:               stringid.GenerateRandomID(),
   227  		buildArgs:        buildConfig.BuildArgs,
   228  		allowedBuildArgs: make(map[string]bool),
   229  	}
   230  
   231  	defer func() {
   232  		builder.Daemon.Graph().Release(builder.id, builder.activeImages...)
   233  	}()
   234  
   235  	id, err := builder.Run(context)
   236  	if err != nil {
   237  		return err
   238  	}
   239  	if repoName != "" {
   240  		return d.Repositories().Tag(repoName, tag, id, true)
   241  	}
   242  	return nil
   243  }
   244  
   245  // BuildFromConfig will do build directly from parameter 'changes', which comes
   246  // from Dockerfile entries, it will:
   247  //
   248  // - call parse.Parse() to get AST root from Dockerfile entries
   249  // - do build by calling builder.dispatch() to call all entries' handling routines
   250  func BuildFromConfig(d *daemon.Daemon, c *runconfig.Config, changes []string) (*runconfig.Config, error) {
   251  	ast, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	// ensure that the commands are valid
   257  	for _, n := range ast.Children {
   258  		if !validCommitCommands[n.Value] {
   259  			return nil, fmt.Errorf("%s is not a valid change command", n.Value)
   260  		}
   261  	}
   262  
   263  	builder := &builder{
   264  		Daemon:        d,
   265  		Config:        c,
   266  		OutStream:     ioutil.Discard,
   267  		ErrStream:     ioutil.Discard,
   268  		disableCommit: true,
   269  	}
   270  
   271  	for i, n := range ast.Children {
   272  		if err := builder.dispatch(i, n); err != nil {
   273  			return nil, err
   274  		}
   275  	}
   276  
   277  	return builder.Config, nil
   278  }
   279  
   280  // CommitConfig contains build configs for commit operation
   281  type CommitConfig struct {
   282  	Pause   bool
   283  	Repo    string
   284  	Tag     string
   285  	Author  string
   286  	Comment string
   287  	Changes []string
   288  	Config  *runconfig.Config
   289  }
   290  
   291  // Commit will create a new image from a container's changes
   292  func Commit(name string, d *daemon.Daemon, c *CommitConfig) (string, error) {
   293  	container, err := d.Get(name)
   294  	if err != nil {
   295  		return "", err
   296  	}
   297  
   298  	// It is not possible to commit a running container on Windows
   299  	if runtime.GOOS == "windows" && container.IsRunning() {
   300  		return "", fmt.Errorf("Windows does not support commit of a running container")
   301  	}
   302  
   303  	if c.Config == nil {
   304  		c.Config = &runconfig.Config{}
   305  	}
   306  
   307  	newConfig, err := BuildFromConfig(d, c.Config, c.Changes)
   308  	if err != nil {
   309  		return "", err
   310  	}
   311  
   312  	if err := runconfig.Merge(newConfig, container.Config); err != nil {
   313  		return "", err
   314  	}
   315  
   316  	commitCfg := &daemon.ContainerCommitConfig{
   317  		Pause:   c.Pause,
   318  		Repo:    c.Repo,
   319  		Tag:     c.Tag,
   320  		Author:  c.Author,
   321  		Comment: c.Comment,
   322  		Config:  newConfig,
   323  	}
   324  
   325  	img, err := d.Commit(container, commitCfg)
   326  	if err != nil {
   327  		return "", err
   328  	}
   329  
   330  	return img.ID, nil
   331  }
   332  
   333  // inspectResponse looks into the http response data at r to determine whether its
   334  // content-type is on the list of acceptable content types for remote build contexts.
   335  // This function returns:
   336  //    - a string representation of the detected content-type
   337  //    - an io.Reader for the response body
   338  //    - an error value which will be non-nil either when something goes wrong while
   339  //      reading bytes from r or when the detected content-type is not acceptable.
   340  func inspectResponse(ct string, r io.ReadCloser, clen int64) (string, io.ReadCloser, error) {
   341  	plen := clen
   342  	if plen <= 0 || plen > maxPreambleLength {
   343  		plen = maxPreambleLength
   344  	}
   345  
   346  	preamble := make([]byte, plen, plen)
   347  	rlen, err := r.Read(preamble)
   348  	if rlen == 0 {
   349  		return ct, r, errors.New("Empty response")
   350  	}
   351  	if err != nil && err != io.EOF {
   352  		return ct, r, err
   353  	}
   354  
   355  	preambleR := bytes.NewReader(preamble)
   356  	bodyReader := ioutil.NopCloser(io.MultiReader(preambleR, r))
   357  	// Some web servers will use application/octet-stream as the default
   358  	// content type for files without an extension (e.g. 'Dockerfile')
   359  	// so if we receive this value we better check for text content
   360  	contentType := ct
   361  	if len(ct) == 0 || ct == httputils.MimeTypes.OctetStream {
   362  		contentType, _, err = httputils.DetectContentType(preamble)
   363  		if err != nil {
   364  			return contentType, bodyReader, err
   365  		}
   366  	}
   367  
   368  	contentType = selectAcceptableMIME(contentType)
   369  	var cterr error
   370  	if len(contentType) == 0 {
   371  		cterr = fmt.Errorf("unsupported Content-Type %q", ct)
   372  		contentType = ct
   373  	}
   374  
   375  	return contentType, bodyReader, cterr
   376  }