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