github.com/afein/docker@v1.8.2/builder/job.go (about)

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