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