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