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