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