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