github.com/rhatdan/docker@v0.7.7-0.20180119204836-47a0dcbcd20a/builder/dockerfile/builder.go (about) 1 package dockerfile 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "runtime" 9 "strings" 10 "time" 11 12 "github.com/docker/docker/api/types" 13 "github.com/docker/docker/api/types/backend" 14 "github.com/docker/docker/api/types/container" 15 "github.com/docker/docker/builder" 16 "github.com/docker/docker/builder/dockerfile/instructions" 17 "github.com/docker/docker/builder/dockerfile/parser" 18 "github.com/docker/docker/builder/fscache" 19 "github.com/docker/docker/builder/remotecontext" 20 "github.com/docker/docker/errdefs" 21 "github.com/docker/docker/pkg/idtools" 22 "github.com/docker/docker/pkg/streamformatter" 23 "github.com/docker/docker/pkg/stringid" 24 "github.com/docker/docker/pkg/system" 25 "github.com/moby/buildkit/session" 26 "github.com/pkg/errors" 27 "github.com/sirupsen/logrus" 28 "golang.org/x/net/context" 29 "golang.org/x/sync/syncmap" 30 ) 31 32 var validCommitCommands = map[string]bool{ 33 "cmd": true, 34 "entrypoint": true, 35 "healthcheck": true, 36 "env": true, 37 "expose": true, 38 "label": true, 39 "onbuild": true, 40 "user": true, 41 "volume": true, 42 "workdir": true, 43 } 44 45 const ( 46 stepFormat = "Step %d/%d : %v" 47 ) 48 49 // SessionGetter is object used to get access to a session by uuid 50 type SessionGetter interface { 51 Get(ctx context.Context, uuid string) (session.Caller, error) 52 } 53 54 // BuildManager is shared across all Builder objects 55 type BuildManager struct { 56 idMappings *idtools.IDMappings 57 backend builder.Backend 58 pathCache pathCache // TODO: make this persistent 59 sg SessionGetter 60 fsCache *fscache.FSCache 61 } 62 63 // NewBuildManager creates a BuildManager 64 func NewBuildManager(b builder.Backend, sg SessionGetter, fsCache *fscache.FSCache, idMappings *idtools.IDMappings) (*BuildManager, error) { 65 bm := &BuildManager{ 66 backend: b, 67 pathCache: &syncmap.Map{}, 68 sg: sg, 69 idMappings: idMappings, 70 fsCache: fsCache, 71 } 72 if err := fsCache.RegisterTransport(remotecontext.ClientSessionRemote, NewClientSessionTransport()); err != nil { 73 return nil, err 74 } 75 return bm, nil 76 } 77 78 // Build starts a new build from a BuildConfig 79 func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) { 80 buildsTriggered.Inc() 81 if config.Options.Dockerfile == "" { 82 config.Options.Dockerfile = builder.DefaultDockerfileName 83 } 84 85 source, dockerfile, err := remotecontext.Detect(config) 86 if err != nil { 87 return nil, err 88 } 89 defer func() { 90 if source != nil { 91 if err := source.Close(); err != nil { 92 logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err) 93 } 94 } 95 }() 96 97 ctx, cancel := context.WithCancel(ctx) 98 defer cancel() 99 100 if src, err := bm.initializeClientSession(ctx, cancel, config.Options); err != nil { 101 return nil, err 102 } else if src != nil { 103 source = src 104 } 105 106 os := runtime.GOOS 107 optionsPlatform := system.ParsePlatform(config.Options.Platform) 108 if dockerfile.OS != "" { 109 if optionsPlatform.OS != "" && optionsPlatform.OS != dockerfile.OS { 110 return nil, fmt.Errorf("invalid platform") 111 } 112 os = dockerfile.OS 113 } else if optionsPlatform.OS != "" { 114 os = optionsPlatform.OS 115 } 116 config.Options.Platform = os 117 dockerfile.OS = os 118 119 builderOptions := builderOptions{ 120 Options: config.Options, 121 ProgressWriter: config.ProgressWriter, 122 Backend: bm.backend, 123 PathCache: bm.pathCache, 124 IDMappings: bm.idMappings, 125 } 126 return newBuilder(ctx, builderOptions).build(source, dockerfile) 127 } 128 129 func (bm *BuildManager) initializeClientSession(ctx context.Context, cancel func(), options *types.ImageBuildOptions) (builder.Source, error) { 130 if options.SessionID == "" || bm.sg == nil { 131 return nil, nil 132 } 133 logrus.Debug("client is session enabled") 134 135 connectCtx, cancelCtx := context.WithTimeout(ctx, sessionConnectTimeout) 136 defer cancelCtx() 137 138 c, err := bm.sg.Get(connectCtx, options.SessionID) 139 if err != nil { 140 return nil, err 141 } 142 go func() { 143 <-c.Context().Done() 144 cancel() 145 }() 146 if options.RemoteContext == remotecontext.ClientSessionRemote { 147 st := time.Now() 148 csi, err := NewClientSessionSourceIdentifier(ctx, bm.sg, options.SessionID) 149 if err != nil { 150 return nil, err 151 } 152 src, err := bm.fsCache.SyncFrom(ctx, csi) 153 if err != nil { 154 return nil, err 155 } 156 logrus.Debugf("sync-time: %v", time.Since(st)) 157 return src, nil 158 } 159 return nil, nil 160 } 161 162 // builderOptions are the dependencies required by the builder 163 type builderOptions struct { 164 Options *types.ImageBuildOptions 165 Backend builder.Backend 166 ProgressWriter backend.ProgressWriter 167 PathCache pathCache 168 IDMappings *idtools.IDMappings 169 } 170 171 // Builder is a Dockerfile builder 172 // It implements the builder.Backend interface. 173 type Builder struct { 174 options *types.ImageBuildOptions 175 176 Stdout io.Writer 177 Stderr io.Writer 178 Aux *streamformatter.AuxFormatter 179 Output io.Writer 180 181 docker builder.Backend 182 clientCtx context.Context 183 184 idMappings *idtools.IDMappings 185 disableCommit bool 186 imageSources *imageSources 187 pathCache pathCache 188 containerManager *containerManager 189 imageProber ImageProber 190 } 191 192 // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options. 193 func newBuilder(clientCtx context.Context, options builderOptions) *Builder { 194 config := options.Options 195 if config == nil { 196 config = new(types.ImageBuildOptions) 197 } 198 199 b := &Builder{ 200 clientCtx: clientCtx, 201 options: config, 202 Stdout: options.ProgressWriter.StdoutFormatter, 203 Stderr: options.ProgressWriter.StderrFormatter, 204 Aux: options.ProgressWriter.AuxFormatter, 205 Output: options.ProgressWriter.Output, 206 docker: options.Backend, 207 idMappings: options.IDMappings, 208 imageSources: newImageSources(clientCtx, options), 209 pathCache: options.PathCache, 210 imageProber: newImageProber(options.Backend, config.CacheFrom, config.NoCache), 211 containerManager: newContainerManager(options.Backend), 212 } 213 214 return b 215 } 216 217 // Build runs the Dockerfile builder by parsing the Dockerfile and executing 218 // the instructions from the file. 219 func (b *Builder) build(source builder.Source, dockerfile *parser.Result) (*builder.Result, error) { 220 defer b.imageSources.Unmount() 221 222 addNodesForLabelOption(dockerfile.AST, b.options.Labels) 223 224 stages, metaArgs, err := instructions.Parse(dockerfile.AST) 225 if err != nil { 226 if instructions.IsUnknownInstruction(err) { 227 buildsFailed.WithValues(metricsUnknownInstructionError).Inc() 228 } 229 return nil, errdefs.InvalidParameter(err) 230 } 231 if b.options.Target != "" { 232 targetIx, found := instructions.HasStage(stages, b.options.Target) 233 if !found { 234 buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc() 235 return nil, errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target) 236 } 237 stages = stages[:targetIx+1] 238 } 239 240 dockerfile.PrintWarnings(b.Stderr) 241 dispatchState, err := b.dispatchDockerfileWithCancellation(stages, metaArgs, dockerfile.EscapeToken, source) 242 if err != nil { 243 return nil, err 244 } 245 if dispatchState.imageID == "" { 246 buildsFailed.WithValues(metricsDockerfileEmptyError).Inc() 247 return nil, errors.New("No image was generated. Is your Dockerfile empty?") 248 } 249 return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil 250 } 251 252 func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error { 253 if aux == nil || state.imageID == "" { 254 return nil 255 } 256 return aux.Emit(types.BuildResult{ID: state.imageID}) 257 } 258 259 func processMetaArg(meta instructions.ArgCommand, shlex *ShellLex, args *buildArgs) error { 260 // ShellLex currently only support the concatenated string format 261 envs := convertMapToEnvList(args.GetAllAllowed()) 262 if err := meta.Expand(func(word string) (string, error) { 263 return shlex.ProcessWord(word, envs) 264 }); err != nil { 265 return err 266 } 267 args.AddArg(meta.Key, meta.Value) 268 args.AddMetaArg(meta.Key, meta.Value) 269 return nil 270 } 271 272 func printCommand(out io.Writer, currentCommandIndex int, totalCommands int, cmd interface{}) int { 273 fmt.Fprintf(out, stepFormat, currentCommandIndex, totalCommands, cmd) 274 fmt.Fprintln(out) 275 return currentCommandIndex + 1 276 } 277 278 func (b *Builder) dispatchDockerfileWithCancellation(parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) { 279 dispatchRequest := dispatchRequest{} 280 buildArgs := newBuildArgs(b.options.BuildArgs) 281 totalCommands := len(metaArgs) + len(parseResult) 282 currentCommandIndex := 1 283 for _, stage := range parseResult { 284 totalCommands += len(stage.Commands) 285 } 286 shlex := NewShellLex(escapeToken) 287 for _, meta := range metaArgs { 288 currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &meta) 289 290 err := processMetaArg(meta, shlex, buildArgs) 291 if err != nil { 292 return nil, err 293 } 294 } 295 296 stagesResults := newStagesBuildResults() 297 298 for _, stage := range parseResult { 299 if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil { 300 return nil, err 301 } 302 dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults) 303 304 currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode) 305 if err := initializeStage(dispatchRequest, &stage); err != nil { 306 return nil, err 307 } 308 dispatchRequest.state.updateRunConfig() 309 fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID)) 310 for _, cmd := range stage.Commands { 311 select { 312 case <-b.clientCtx.Done(): 313 logrus.Debug("Builder: build cancelled!") 314 fmt.Fprint(b.Stdout, "Build cancelled\n") 315 buildsFailed.WithValues(metricsBuildCanceled).Inc() 316 return nil, errors.New("Build cancelled") 317 default: 318 // Not cancelled yet, keep going... 319 } 320 321 currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd) 322 323 if err := dispatch(dispatchRequest, cmd); err != nil { 324 return nil, err 325 } 326 dispatchRequest.state.updateRunConfig() 327 fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID)) 328 329 } 330 if err := emitImageID(b.Aux, dispatchRequest.state); err != nil { 331 return nil, err 332 } 333 buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs) 334 if err := commitStage(dispatchRequest.state, stagesResults); err != nil { 335 return nil, err 336 } 337 } 338 buildArgs.WarnOnUnusedBuildArgs(b.Stdout) 339 return dispatchRequest.state, nil 340 } 341 342 func addNodesForLabelOption(dockerfile *parser.Node, labels map[string]string) { 343 if len(labels) == 0 { 344 return 345 } 346 347 node := parser.NodeFromLabels(labels) 348 dockerfile.Children = append(dockerfile.Children, node) 349 } 350 351 // BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile 352 // It will: 353 // - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries. 354 // - Do build by calling builder.dispatch() to call all entries' handling routines 355 // 356 // BuildFromConfig is used by the /commit endpoint, with the changes 357 // coming from the query parameter of the same name. 358 // 359 // TODO: Remove? 360 func BuildFromConfig(config *container.Config, changes []string, os string) (*container.Config, error) { 361 if !system.IsOSSupported(os) { 362 return nil, errdefs.InvalidParameter(system.ErrNotSupportedOperatingSystem) 363 } 364 if len(changes) == 0 { 365 return config, nil 366 } 367 368 dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n"))) 369 if err != nil { 370 return nil, errdefs.InvalidParameter(err) 371 } 372 373 b := newBuilder(context.Background(), builderOptions{ 374 Options: &types.ImageBuildOptions{NoCache: true}, 375 }) 376 377 // ensure that the commands are valid 378 for _, n := range dockerfile.AST.Children { 379 if !validCommitCommands[n.Value] { 380 return nil, errdefs.InvalidParameter(errors.Errorf("%s is not a valid change command", n.Value)) 381 } 382 } 383 384 b.Stdout = ioutil.Discard 385 b.Stderr = ioutil.Discard 386 b.disableCommit = true 387 388 commands := []instructions.Command{} 389 for _, n := range dockerfile.AST.Children { 390 cmd, err := instructions.ParseCommand(n) 391 if err != nil { 392 return nil, errdefs.InvalidParameter(err) 393 } 394 commands = append(commands, cmd) 395 } 396 397 dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, newBuildArgs(b.options.BuildArgs), newStagesBuildResults()) 398 // We make mutations to the configuration, ensure we have a copy 399 dispatchRequest.state.runConfig = copyRunConfig(config) 400 dispatchRequest.state.imageID = config.Image 401 dispatchRequest.state.operatingSystem = os 402 for _, cmd := range commands { 403 err := dispatch(dispatchRequest, cmd) 404 if err != nil { 405 return nil, errdefs.InvalidParameter(err) 406 } 407 dispatchRequest.state.updateRunConfig() 408 } 409 410 return dispatchRequest.state.runConfig, nil 411 } 412 413 func convertMapToEnvList(m map[string]string) []string { 414 result := []string{} 415 for k, v := range m { 416 result = append(result, k+"="+v) 417 } 418 return result 419 }