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