123ade3763
The build target is not quoted and it makes it difficult for some persons to see what the problem is. By quoting it we emphasize that the target name is variable. Signed-off-by: Frank Villaro-Dixon <frank.villarodixon@merkle.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/platforms"
|
|
"github.com/containerd/log"
|
|
"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("target stage %q could not be found", 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
|
|
}
|