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 }