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