74da6a6363
This unifies our logging and allows us to propagate logging and trace contexts together. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
382 lines
12 KiB
Go
382 lines
12 KiB
Go
package dockerfile // import "github.com/docker/docker/builder/dockerfile"
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/containerd/containerd/log"
|
|
"github.com/containerd/containerd/platforms"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/backend"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/builder"
|
|
"github.com/docker/docker/builder/remotecontext"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/pkg/idtools"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
"github.com/moby/buildkit/frontend/dockerfile/instructions"
|
|
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
|
"github.com/moby/buildkit/frontend/dockerfile/shell"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/sync/syncmap"
|
|
)
|
|
|
|
var validCommitCommands = map[string]bool{
|
|
"cmd": true,
|
|
"entrypoint": true,
|
|
"healthcheck": true,
|
|
"env": true,
|
|
"expose": true,
|
|
"label": true,
|
|
"onbuild": true,
|
|
"stopsignal": true,
|
|
"user": true,
|
|
"volume": true,
|
|
"workdir": true,
|
|
}
|
|
|
|
const (
|
|
stepFormat = "Step %d/%d : %v"
|
|
)
|
|
|
|
// BuildManager is shared across all Builder objects
|
|
type BuildManager struct {
|
|
idMapping idtools.IdentityMapping
|
|
backend builder.Backend
|
|
pathCache pathCache // TODO: make this persistent
|
|
}
|
|
|
|
// NewBuildManager creates a BuildManager
|
|
func NewBuildManager(b builder.Backend, identityMapping idtools.IdentityMapping) (*BuildManager, error) {
|
|
bm := &BuildManager{
|
|
backend: b,
|
|
pathCache: &syncmap.Map{},
|
|
idMapping: identityMapping,
|
|
}
|
|
return bm, nil
|
|
}
|
|
|
|
// Build starts a new build from a BuildConfig
|
|
func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) (*builder.Result, error) {
|
|
buildsTriggered.Inc()
|
|
if config.Options.Dockerfile == "" {
|
|
config.Options.Dockerfile = builder.DefaultDockerfileName
|
|
}
|
|
|
|
source, dockerfile, err := remotecontext.Detect(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if source != nil {
|
|
if err := source.Close(); err != nil {
|
|
log.G(ctx).Debugf("[BUILDER] failed to remove temporary context: %v", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
builderOptions := builderOptions{
|
|
Options: config.Options,
|
|
ProgressWriter: config.ProgressWriter,
|
|
Backend: bm.backend,
|
|
PathCache: bm.pathCache,
|
|
IDMapping: bm.idMapping,
|
|
}
|
|
b, err := newBuilder(ctx, builderOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return b.build(ctx, source, dockerfile)
|
|
}
|
|
|
|
// builderOptions are the dependencies required by the builder
|
|
type builderOptions struct {
|
|
Options *types.ImageBuildOptions
|
|
Backend builder.Backend
|
|
ProgressWriter backend.ProgressWriter
|
|
PathCache pathCache
|
|
IDMapping idtools.IdentityMapping
|
|
}
|
|
|
|
// Builder is a Dockerfile builder
|
|
// It implements the builder.Backend interface.
|
|
type Builder struct {
|
|
options *types.ImageBuildOptions
|
|
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
Aux *streamformatter.AuxFormatter
|
|
Output io.Writer
|
|
|
|
docker builder.Backend
|
|
|
|
idMapping idtools.IdentityMapping
|
|
disableCommit bool
|
|
imageSources *imageSources
|
|
pathCache pathCache
|
|
containerManager *containerManager
|
|
imageProber ImageProber
|
|
platform *ocispec.Platform
|
|
}
|
|
|
|
// newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options.
|
|
func newBuilder(ctx context.Context, options builderOptions) (*Builder, error) {
|
|
config := options.Options
|
|
if config == nil {
|
|
config = new(types.ImageBuildOptions)
|
|
}
|
|
|
|
imageProber, err := newImageProber(ctx, options.Backend, config.CacheFrom, config.NoCache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b := &Builder{
|
|
options: config,
|
|
Stdout: options.ProgressWriter.StdoutFormatter,
|
|
Stderr: options.ProgressWriter.StderrFormatter,
|
|
Aux: options.ProgressWriter.AuxFormatter,
|
|
Output: options.ProgressWriter.Output,
|
|
docker: options.Backend,
|
|
idMapping: options.IDMapping,
|
|
imageSources: newImageSources(options),
|
|
pathCache: options.PathCache,
|
|
imageProber: imageProber,
|
|
containerManager: newContainerManager(options.Backend),
|
|
}
|
|
|
|
// same as in Builder.Build in builder/builder-next/builder.go
|
|
// TODO: remove once config.Platform is of type specs.Platform
|
|
if config.Platform != "" {
|
|
sp, err := platforms.Parse(config.Platform)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b.platform = &sp
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// Build 'LABEL' command(s) from '--label' options and add to the last stage
|
|
func buildLabelOptions(labels map[string]string, stages []instructions.Stage) {
|
|
keys := []string{}
|
|
for key := range labels {
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
// Sort the label to have a repeatable order
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
value := labels[key]
|
|
stages[len(stages)-1].AddCommand(instructions.NewLabelCommand(key, value, true))
|
|
}
|
|
}
|
|
|
|
// Build runs the Dockerfile builder by parsing the Dockerfile and executing
|
|
// the instructions from the file.
|
|
func (b *Builder) build(ctx context.Context, source builder.Source, dockerfile *parser.Result) (*builder.Result, error) {
|
|
defer b.imageSources.Unmount()
|
|
|
|
stages, metaArgs, err := instructions.Parse(dockerfile.AST)
|
|
if err != nil {
|
|
var uiErr *instructions.UnknownInstructionError
|
|
if errors.As(err, &uiErr) {
|
|
buildsFailed.WithValues(metricsUnknownInstructionError).Inc()
|
|
}
|
|
return nil, errdefs.InvalidParameter(err)
|
|
}
|
|
if b.options.Target != "" {
|
|
targetIx, found := instructions.HasStage(stages, b.options.Target)
|
|
if !found {
|
|
buildsFailed.WithValues(metricsBuildTargetNotReachableError).Inc()
|
|
return nil, errdefs.InvalidParameter(errors.Errorf("failed to reach build target %s in Dockerfile", b.options.Target))
|
|
}
|
|
stages = stages[:targetIx+1]
|
|
}
|
|
|
|
// Add 'LABEL' command specified by '--label' option to the last stage
|
|
buildLabelOptions(b.options.Labels, stages)
|
|
|
|
dockerfile.PrintWarnings(b.Stderr)
|
|
dispatchState, err := b.dispatchDockerfileWithCancellation(ctx, stages, metaArgs, dockerfile.EscapeToken, source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dispatchState.imageID == "" {
|
|
buildsFailed.WithValues(metricsDockerfileEmptyError).Inc()
|
|
return nil, errors.New("No image was generated. Is your Dockerfile empty?")
|
|
}
|
|
return &builder.Result{ImageID: dispatchState.imageID, FromImage: dispatchState.baseImage}, nil
|
|
}
|
|
|
|
func emitImageID(aux *streamformatter.AuxFormatter, state *dispatchState) error {
|
|
if aux == nil || state.imageID == "" {
|
|
return nil
|
|
}
|
|
return aux.Emit("", types.BuildResult{ID: state.imageID})
|
|
}
|
|
|
|
func processMetaArg(meta instructions.ArgCommand, shlex *shell.Lex, args *BuildArgs) error {
|
|
// shell.Lex currently only support the concatenated string format
|
|
envs := convertMapToEnvList(args.GetAllAllowed())
|
|
if err := meta.Expand(func(word string) (string, error) {
|
|
return shlex.ProcessWord(word, envs)
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
for _, arg := range meta.Args {
|
|
args.AddArg(arg.Key, arg.Value)
|
|
args.AddMetaArg(arg.Key, arg.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func printCommand(out io.Writer, currentCommandIndex int, totalCommands int, cmd interface{}) int {
|
|
fmt.Fprintf(out, stepFormat, currentCommandIndex, totalCommands, cmd)
|
|
fmt.Fprintln(out)
|
|
return currentCommandIndex + 1
|
|
}
|
|
|
|
func (b *Builder) dispatchDockerfileWithCancellation(ctx context.Context, parseResult []instructions.Stage, metaArgs []instructions.ArgCommand, escapeToken rune, source builder.Source) (*dispatchState, error) {
|
|
dispatchRequest := dispatchRequest{}
|
|
buildArgs := NewBuildArgs(b.options.BuildArgs)
|
|
totalCommands := len(metaArgs) + len(parseResult)
|
|
currentCommandIndex := 1
|
|
for _, stage := range parseResult {
|
|
totalCommands += len(stage.Commands)
|
|
}
|
|
shlex := shell.NewLex(escapeToken)
|
|
for i := range metaArgs {
|
|
currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, &metaArgs[i])
|
|
|
|
err := processMetaArg(metaArgs[i], shlex, buildArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
stagesResults := newStagesBuildResults()
|
|
|
|
for _, s := range parseResult {
|
|
stage := s
|
|
if err := stagesResults.checkStageNameAvailable(stage.Name); err != nil {
|
|
return nil, err
|
|
}
|
|
dispatchRequest = newDispatchRequest(b, escapeToken, source, buildArgs, stagesResults)
|
|
|
|
currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, stage.SourceCode)
|
|
if err := initializeStage(ctx, dispatchRequest, &stage); err != nil {
|
|
return nil, err
|
|
}
|
|
dispatchRequest.state.updateRunConfig()
|
|
fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
|
|
for _, cmd := range stage.Commands {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.G(ctx).Debug("Builder: build cancelled!")
|
|
fmt.Fprint(b.Stdout, "Build cancelled\n")
|
|
buildsFailed.WithValues(metricsBuildCanceled).Inc()
|
|
return nil, errors.New("Build cancelled")
|
|
default:
|
|
// Not cancelled yet, keep going...
|
|
}
|
|
|
|
currentCommandIndex = printCommand(b.Stdout, currentCommandIndex, totalCommands, cmd)
|
|
|
|
if err := dispatch(ctx, dispatchRequest, cmd); err != nil {
|
|
return nil, err
|
|
}
|
|
dispatchRequest.state.updateRunConfig()
|
|
fmt.Fprintf(b.Stdout, " ---> %s\n", stringid.TruncateID(dispatchRequest.state.imageID))
|
|
}
|
|
if err := emitImageID(b.Aux, dispatchRequest.state); err != nil {
|
|
return nil, err
|
|
}
|
|
buildArgs.MergeReferencedArgs(dispatchRequest.state.buildArgs)
|
|
if err := commitStage(dispatchRequest.state, stagesResults); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
buildArgs.WarnOnUnusedBuildArgs(b.Stdout)
|
|
return dispatchRequest.state, nil
|
|
}
|
|
|
|
// BuildFromConfig builds directly from `changes`, treating it as if it were the contents of a Dockerfile
|
|
// It will:
|
|
// - Call parse.Parse() to get an AST root for the concatenated Dockerfile entries.
|
|
// - Do build by calling builder.dispatch() to call all entries' handling routines
|
|
//
|
|
// BuildFromConfig is used by the /commit endpoint, with the changes
|
|
// coming from the query parameter of the same name.
|
|
//
|
|
// TODO: Remove?
|
|
func BuildFromConfig(ctx context.Context, config *container.Config, changes []string, os string) (*container.Config, error) {
|
|
if len(changes) == 0 {
|
|
return config, nil
|
|
}
|
|
|
|
dockerfile, err := parser.Parse(bytes.NewBufferString(strings.Join(changes, "\n")))
|
|
if err != nil {
|
|
return nil, errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
b, err := newBuilder(ctx, builderOptions{
|
|
Options: &types.ImageBuildOptions{NoCache: true},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// ensure that the commands are valid
|
|
for _, n := range dockerfile.AST.Children {
|
|
if !validCommitCommands[strings.ToLower(n.Value)] {
|
|
return nil, errdefs.InvalidParameter(errors.Errorf("%s is not a valid change command", n.Value))
|
|
}
|
|
}
|
|
|
|
b.Stdout = io.Discard
|
|
b.Stderr = io.Discard
|
|
b.disableCommit = true
|
|
|
|
var commands []instructions.Command
|
|
for _, n := range dockerfile.AST.Children {
|
|
cmd, err := instructions.ParseCommand(n)
|
|
if err != nil {
|
|
return nil, errdefs.InvalidParameter(err)
|
|
}
|
|
commands = append(commands, cmd)
|
|
}
|
|
|
|
dispatchRequest := newDispatchRequest(b, dockerfile.EscapeToken, nil, NewBuildArgs(b.options.BuildArgs), newStagesBuildResults())
|
|
// We make mutations to the configuration, ensure we have a copy
|
|
dispatchRequest.state.runConfig = copyRunConfig(config)
|
|
dispatchRequest.state.imageID = config.Image
|
|
dispatchRequest.state.operatingSystem = os
|
|
for _, cmd := range commands {
|
|
err := dispatch(ctx, dispatchRequest, cmd)
|
|
if err != nil {
|
|
return nil, errdefs.InvalidParameter(err)
|
|
}
|
|
dispatchRequest.state.updateRunConfig()
|
|
}
|
|
|
|
return dispatchRequest.state.runConfig, nil
|
|
}
|
|
|
|
func convertMapToEnvList(m map[string]string) []string {
|
|
result := []string{}
|
|
for k, v := range m {
|
|
result = append(result, k+"="+v)
|
|
}
|
|
return result
|
|
}
|