diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index d20bc0403a..d328235a14 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "io/ioutil" - "runtime" "strings" "time" @@ -104,18 +103,12 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) ( source = src } - os := runtime.GOOS - optionsPlatform := system.ParsePlatform(config.Options.Platform) - if dockerfile.OS != "" { - if optionsPlatform.OS != "" && optionsPlatform.OS != dockerfile.OS { - return nil, fmt.Errorf("invalid platform") - } - os = dockerfile.OS - } else if optionsPlatform.OS != "" { - os = optionsPlatform.OS + os := "" + apiPlatform := system.ParsePlatform(config.Options.Platform) + if apiPlatform.OS != "" { + os = apiPlatform.OS } config.Options.Platform = os - dockerfile.OS = os builderOptions := builderOptions{ Options: config.Options, diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go index 99d4aa627e..991c433b2b 100644 --- a/builder/dockerfile/dispatchers.go +++ b/builder/dockerfile/dispatchers.go @@ -145,14 +145,17 @@ func (d *dispatchRequest) getImageMount(imageRefOrID string) (*imageMount, error imageRefOrID = stage.Image localOnly = true } - return d.builder.imageSources.Get(imageRefOrID, localOnly) + return d.builder.imageSources.Get(imageRefOrID, localOnly, d.state.operatingSystem) } -// FROM imagename[:tag | @digest] [AS build-stage-name] +// FROM [--platform=platform] imagename[:tag | @digest] [AS build-stage-name] // func initializeStage(d dispatchRequest, cmd *instructions.Stage) error { d.builder.imageProber.Reset() - image, err := d.getFromImage(d.shlex, cmd.BaseName) + if err := system.ValidatePlatform(&cmd.Platform); err != nil { + return err + } + image, err := d.getFromImage(d.shlex, cmd.BaseName, cmd.Platform.OS) if err != nil { return err } @@ -210,20 +213,41 @@ func (d *dispatchRequest) getExpandedImageName(shlex *shell.Lex, name string) (s } return name, nil } -func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) { + +// getOsFromFlagsAndStage calculates the operating system if we need to pull an image. +// stagePlatform contains the value supplied by optional `--platform=` on +// a current FROM statement. b.builder.options.Platform contains the operating +// system part of the optional flag passed in the API call (or CLI flag +// through `docker build --platform=...`). Precedence is for an explicit +// platform indication in the FROM statement. +func (d *dispatchRequest) getOsFromFlagsAndStage(stageOS string) string { + switch { + case stageOS != "": + return stageOS + case d.builder.options.Platform != "": + // Note this is API "platform", but by this point, as the daemon is not + // multi-arch aware yet, it is guaranteed to only hold the OS part here. + return d.builder.options.Platform + default: + return runtime.GOOS + } +} + +func (d *dispatchRequest) getImageOrStage(name string, stageOS string) (builder.Image, error) { var localOnly bool if im, ok := d.stages.getByName(name); ok { name = im.Image localOnly = true } + os := d.getOsFromFlagsAndStage(stageOS) + // Windows cannot support a container with no base image unless it is LCOW. if name == api.NoBaseImageSpecifier { imageImage := &image.Image{} imageImage.OS = runtime.GOOS if runtime.GOOS == "windows" { - optionsOS := system.ParsePlatform(d.builder.options.Platform).OS - switch optionsOS { + switch os { case "windows", "": return nil, errors.New("Windows does not support FROM scratch") case "linux": @@ -232,23 +256,23 @@ func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) { } imageImage.OS = "linux" default: - return nil, errors.Errorf("operating system %q is not supported", optionsOS) + return nil, errors.Errorf("operating system %q is not supported", os) } } return builder.Image(imageImage), nil } - imageMount, err := d.builder.imageSources.Get(name, localOnly) + imageMount, err := d.builder.imageSources.Get(name, localOnly, os) if err != nil { return nil, err } return imageMount.Image(), nil } -func (d *dispatchRequest) getFromImage(shlex *shell.Lex, name string) (builder.Image, error) { +func (d *dispatchRequest) getFromImage(shlex *shell.Lex, name string, stageOS string) (builder.Image, error) { name, err := d.getExpandedImageName(shlex, name) if err != nil { return nil, err } - return d.getImageOrStage(name) + return d.getImageOrStage(name, stageOS) } func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { @@ -264,8 +288,7 @@ func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { runConfig := d.state.runConfig var err error - baseImageOS := system.ParsePlatform(d.state.operatingSystem).OS - runConfig.WorkingDir, err = normalizeWorkdir(baseImageOS, runConfig.WorkingDir, c.Path) + runConfig.WorkingDir, err = normalizeWorkdir(d.state.operatingSystem, runConfig.WorkingDir, c.Path) if err != nil { return err } @@ -281,7 +304,7 @@ func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { } comment := "WORKDIR " + runConfig.WorkingDir - runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, baseImageOS)) + runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.state.operatingSystem)) containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) if err != nil || containerID == "" { return err @@ -397,8 +420,7 @@ func prependEnvOnCmd(buildArgs *buildArgs, buildArgVars []string, cmd strslice.S // func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error { runConfig := d.state.runConfig - optionsOS := system.ParsePlatform(d.builder.options.Platform).OS - cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, optionsOS) + cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem) runConfig.Cmd = cmd // set config as already being escaped, this prevents double escaping on windows runConfig.ArgsEscaped = true @@ -441,8 +463,7 @@ func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) // func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error { runConfig := d.state.runConfig - optionsOS := system.ParsePlatform(d.builder.options.Platform).OS - cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, optionsOS) + cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.state.operatingSystem) runConfig.Entrypoint = cmd if !d.state.cmdSet { runConfig.Cmd = nil diff --git a/builder/dockerfile/dispatchers_test.go b/builder/dockerfile/dispatchers_test.go index 988580cb15..6ddde82d43 100644 --- a/builder/dockerfile/dispatchers_test.go +++ b/builder/dockerfile/dispatchers_test.go @@ -208,6 +208,7 @@ func TestOnbuild(t *testing.T) { func TestWorkdir(t *testing.T) { b := newBuilderWithMockBackend() sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) + sb.state.baseImage = &mockImage{} workingDir := "/app" if runtime.GOOS == "windows" { workingDir = "C:\\app" @@ -224,6 +225,7 @@ func TestWorkdir(t *testing.T) { func TestCmd(t *testing.T) { b := newBuilderWithMockBackend() sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) + sb.state.baseImage = &mockImage{} command := "./executable" cmd := &instructions.CmdCommand{ @@ -281,6 +283,7 @@ func TestHealthcheckCmd(t *testing.T) { func TestEntrypoint(t *testing.T) { b := newBuilderWithMockBackend() sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) + sb.state.baseImage = &mockImage{} entrypointCmd := "/usr/sbin/nginx" cmd := &instructions.EntrypointCommand{ @@ -356,6 +359,7 @@ func TestStopSignal(t *testing.T) { } b := newBuilderWithMockBackend() sb := newDispatchRequest(b, '`', nil, newBuildArgs(make(map[string]*string)), newStagesBuildResults()) + sb.state.baseImage = &mockImage{} signal := "SIGKILL" cmd := &instructions.StopSignalCommand{ diff --git a/builder/dockerfile/evaluator.go b/builder/dockerfile/evaluator.go index 74264faf2e..0f76845086 100644 --- a/builder/dockerfile/evaluator.go +++ b/builder/dockerfile/evaluator.go @@ -37,8 +37,7 @@ import ( func dispatch(d dispatchRequest, cmd instructions.Command) (err error) { if c, ok := cmd.(instructions.PlatformSpecific); ok { - optionsOS := system.ParsePlatform(d.builder.options.Platform).OS - err := c.CheckPlatform(optionsOS) + err := c.CheckPlatform(d.state.operatingSystem) if err != nil { return errdefs.InvalidParameter(err) } diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index 0d4af384af..fd2b942393 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -6,13 +6,12 @@ import ( "github.com/docker/docker/api/types/backend" "github.com/docker/docker/builder" dockerimage "github.com/docker/docker/image" - "github.com/docker/docker/pkg/system" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/net/context" ) -type getAndMountFunc func(string, bool) (builder.Image, builder.ROLayer, error) +type getAndMountFunc func(string, bool, string) (builder.Image, builder.ROLayer, error) // imageSources mounts images and provides a cache for mounted images. It tracks // all images so they can be unmounted at the end of the build. @@ -23,7 +22,7 @@ type imageSources struct { } func newImageSources(ctx context.Context, options builderOptions) *imageSources { - getAndMount := func(idOrRef string, localOnly bool) (builder.Image, builder.ROLayer, error) { + getAndMount := func(idOrRef string, localOnly bool, osForPull string) (builder.Image, builder.ROLayer, error) { pullOption := backend.PullOptionNoPull if !localOnly { if options.Options.PullParent { @@ -32,12 +31,11 @@ func newImageSources(ctx context.Context, options builderOptions) *imageSources pullOption = backend.PullOptionPreferLocal } } - optionsPlatform := system.ParsePlatform(options.Options.Platform) return options.Backend.GetImageAndReleasableLayer(ctx, idOrRef, backend.GetImageAndLayerOptions{ PullOption: pullOption, AuthConfig: options.Options.AuthConfigs, Output: options.ProgressWriter.Output, - OS: optionsPlatform.OS, + OS: osForPull, }) } @@ -47,12 +45,12 @@ func newImageSources(ctx context.Context, options builderOptions) *imageSources } } -func (m *imageSources) Get(idOrRef string, localOnly bool) (*imageMount, error) { +func (m *imageSources) Get(idOrRef string, localOnly bool, osForPull string) (*imageMount, error) { if im, ok := m.byImageID[idOrRef]; ok { return im, nil } - image, layer, err := m.getImage(idOrRef, localOnly) + image, layer, err := m.getImage(idOrRef, localOnly, osForPull) if err != nil { return nil, err } diff --git a/builder/dockerfile/instructions/commands.go b/builder/dockerfile/instructions/commands.go index d4f55ceb43..a10140cf04 100644 --- a/builder/dockerfile/instructions/commands.go +++ b/builder/dockerfile/instructions/commands.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/strslice" + specs "github.com/opencontainers/image-spec/specs-go/v1" ) // KeyValuePair represent an arbitrary named value (useful in slice instead of map[string] string to preserve ordering) @@ -361,6 +362,7 @@ type Stage struct { Commands []Command BaseName string SourceCode string + Platform specs.Platform } // AddCommand to the stage diff --git a/builder/dockerfile/instructions/parse.go b/builder/dockerfile/instructions/parse.go index 9226f4d46e..e2d69a4887 100644 --- a/builder/dockerfile/instructions/parse.go +++ b/builder/dockerfile/instructions/parse.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/builder/dockerfile/command" "github.com/docker/docker/builder/dockerfile/parser" + "github.com/docker/docker/pkg/system" "github.com/pkg/errors" ) @@ -271,16 +272,17 @@ func parseFrom(req parseRequest) (*Stage, error) { return nil, err } + flPlatform := req.flags.AddString("platform", "") if err := req.flags.Parse(); err != nil { return nil, err } code := strings.TrimSpace(req.original) - return &Stage{ BaseName: req.args[0], Name: stageName, SourceCode: code, Commands: []Command{}, + Platform: *system.ParsePlatform(flPlatform.Value), }, nil } diff --git a/builder/dockerfile/instructions/parse_test.go b/builder/dockerfile/instructions/parse_test.go index b084ad11c7..242630f72a 100644 --- a/builder/dockerfile/instructions/parse_test.go +++ b/builder/dockerfile/instructions/parse_test.go @@ -196,5 +196,4 @@ func TestErrorCases(t *testing.T) { _, err = ParseInstruction(n) testutil.ErrorContains(t, err, c.expectedError) } - } diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index c8b34f8f65..53748f0619 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -83,8 +83,7 @@ func (b *Builder) commit(dispatchState *dispatchState, comment string) error { return errors.New("Please provide a source image with `from` prior to commit") } - optionsPlatform := system.ParsePlatform(b.options.Platform) - runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, optionsPlatform.OS)) + runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, dispatchState.operatingSystem)) hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd) if err != nil || hit { return err @@ -164,16 +163,15 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest) // TODO: should this have been using origPaths instead of srcHash in the comment? - optionsPlatform := system.ParsePlatform(b.options.Platform) runConfigWithCommentCmd := copyRunConfig( state.runConfig, - withCmdCommentString(commentStr, optionsPlatform.OS)) + withCmdCommentString(commentStr, state.operatingSystem)) hit, err := b.probeCache(state, runConfigWithCommentCmd) if err != nil || hit { return err } - imageMount, err := b.imageSources.Get(state.imageID, true) + imageMount, err := b.imageSources.Get(state.imageID, true, state.operatingSystem) if err != nil { return errors.Wrapf(err, "failed to get destination image %q", state.imageID) } @@ -184,7 +182,7 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error } defer rwLayer.Release() - destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer, b.options.Platform) + destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, rwLayer, state.operatingSystem) if err != nil { return err } diff --git a/builder/dockerfile/parser/parser.go b/builder/dockerfile/parser/parser.go index 277176ee1c..b065b8a4ea 100644 --- a/builder/dockerfile/parser/parser.go +++ b/builder/dockerfile/parser/parser.go @@ -7,13 +7,11 @@ import ( "fmt" "io" "regexp" - "runtime" "strconv" "strings" "unicode" "github.com/docker/docker/builder/dockerfile/command" - "github.com/docker/docker/pkg/system" "github.com/pkg/errors" ) @@ -81,11 +79,10 @@ func (node *Node) AddChild(child *Node, startLine, endLine int) { } var ( - dispatch map[string]func(string, *Directive) (*Node, map[string]bool, error) - tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`) - tokenEscapeCommand = regexp.MustCompile(`^#[ \t]*escape[ \t]*=[ \t]*(?P.).*$`) - tokenPlatformCommand = regexp.MustCompile(`^#[ \t]*platform[ \t]*=[ \t]*(?P.*)$`) - tokenComment = regexp.MustCompile(`^#.*$`) + dispatch map[string]func(string, *Directive) (*Node, map[string]bool, error) + tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`) + tokenEscapeCommand = regexp.MustCompile(`^#[ \t]*escape[ \t]*=[ \t]*(?P.).*$`) + tokenComment = regexp.MustCompile(`^#.*$`) ) // DefaultEscapeToken is the default escape token @@ -95,11 +92,9 @@ const DefaultEscapeToken = '\\' // parsing directives. type Directive struct { escapeToken rune // Current escape token - platformToken string // Current platform token lineContinuationRegex *regexp.Regexp // Current line continuation regex processingComplete bool // Whether we are done looking for directives escapeSeen bool // Whether the escape directive has been seen - platformSeen bool // Whether the platform directive has been seen } // setEscapeToken sets the default token for escaping characters in a Dockerfile. @@ -112,25 +107,9 @@ func (d *Directive) setEscapeToken(s string) error { return nil } -// setPlatformToken sets the default platform for pulling images in a Dockerfile. -func (d *Directive) setPlatformToken(s string) error { - s = strings.ToLower(s) - valid := []string{runtime.GOOS} - if system.LCOWSupported() { - valid = append(valid, "linux") - } - for _, item := range valid { - if s == item { - d.platformToken = s - return nil - } - } - return fmt.Errorf("invalid PLATFORM '%s'. Must be one of %v", s, valid) -} - -// possibleParserDirective looks for one or more parser directives '# escapeToken=' and -// '# platform='. Parser directives must precede any builder instruction -// or other comments, and cannot be repeated. +// possibleParserDirective looks for parser directives, eg '# escapeToken='. +// Parser directives must precede any builder instruction or other comments, +// and cannot be repeated. func (d *Directive) possibleParserDirective(line string) error { if d.processingComplete { return nil @@ -149,22 +128,6 @@ func (d *Directive) possibleParserDirective(line string) error { } } - // Only recognise a platform token if LCOW is supported - if system.LCOWSupported() { - tpcMatch := tokenPlatformCommand.FindStringSubmatch(strings.ToLower(line)) - if len(tpcMatch) != 0 { - for i, n := range tokenPlatformCommand.SubexpNames() { - if n == "platform" { - if d.platformSeen { - return errors.New("only one platform parser directive can be used") - } - d.platformSeen = true - return d.setPlatformToken(tpcMatch[i]) - } - } - } - } - d.processingComplete = true return nil } @@ -237,10 +200,7 @@ func newNodeFromLine(line string, directive *Directive) (*Node, error) { type Result struct { AST *Node EscapeToken rune - // TODO @jhowardmsft - see https://github.com/moby/moby/issues/34617 - // This next field will be removed in a future update for LCOW support. - OS string - Warnings []string + Warnings []string } // PrintWarnings to the writer @@ -320,7 +280,6 @@ func Parse(rwc io.Reader) (*Result, error) { AST: root, Warnings: warnings, EscapeToken: d.escapeToken, - OS: d.platformToken, }, handleScannerError(scanner.Err()) } diff --git a/daemon/images/image_history.go b/daemon/images/image_history.go index 2b92292631..b4ca25b1b6 100644 --- a/daemon/images/image_history.go +++ b/daemon/images/image_history.go @@ -7,6 +7,7 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/image" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/system" ) // ImageHistory returns a slice of ImageHistory structures for the specified image @@ -31,7 +32,9 @@ func (i *ImageService) ImageHistory(name string) ([]*image.HistoryResponseItem, if len(img.RootFS.DiffIDs) <= layerCounter { return nil, fmt.Errorf("too many non-empty layers in History section") } - + if !system.IsOSSupported(img.OperatingSystem()) { + return nil, system.ErrNotSupportedOperatingSystem + } rootFS.Append(img.RootFS.DiffIDs[layerCounter]) l, err := i.layerStores[img.OperatingSystem()].Get(rootFS.ChainID()) if err != nil { diff --git a/daemon/images/images.go b/daemon/images/images.go index 46056f15b5..49212341c5 100644 --- a/daemon/images/images.go +++ b/daemon/images/images.go @@ -271,7 +271,9 @@ func (i *ImageService) SquashImage(id, parent string) (string, error) { rootFS := image.NewRootFS() parentImg = &image.Image{RootFS: rootFS} } - + if !system.IsOSSupported(img.OperatingSystem()) { + return "", errors.Wrap(err, system.ErrNotSupportedOperatingSystem.Error()) + } l, err := i.layerStores[img.OperatingSystem()].Get(img.RootFS.ChainID()) if err != nil { return "", errors.Wrap(err, "error getting image layer")