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