mirror of https://github.com/docker/compose.git
build: pass BuildOptions around explicitly & fix multi-platform issues
The big change here is to pass around an explicit `*BuildOptions` object as part of Compose operations like `up` & `run` that may or may not do builds. If the options object is `nil`, no builds whatsoever will be attempted. Motivation is to allow for partial rebuilds in the context of an `up` for watch. This was broken and tricky to accomplish because various parts of the Compose APIs mutate the `*Project` for convenience in ways that make it unusable afterwards. (For example, it might set `service.Build = nil` because it's not going to build that service right _then_. But we might still want to build it later!) NOTE: This commit does not actually touch the watch logic. This is all in preparation to make it possible. As part of this, a bunch of code moved around and I eliminated a bunch of partially redundant logic, mostly around multi-platform. Several edge cases have been addressed as part of this: * `DOCKER_DEFAULT_PLATFORM` was _overriding_ explicitly set platforms in some cases, this is no longer true, and it behaves like the Docker CLI now * It was possible for Compose to build an image for one platform and then try to run it for a different platform (and fail) * Errors are no longer returned if a local image exists but for the wrong platform - the correct platform will be fetched/built (if possible). Because there's a LOT of subtlety and tricky logic here, I've also tried to add an excessive amount of explanatory comments. Signed-off-by: Milas Bowman <milas.bowman@docker.com>
This commit is contained in:
parent
407a0d5b53
commit
1fdbcb6255
|
@ -35,7 +35,6 @@ import (
|
|||
|
||||
type buildOptions struct {
|
||||
*ProjectOptions
|
||||
composeOptions
|
||||
quiet bool
|
||||
pull bool
|
||||
push bool
|
||||
|
@ -73,7 +72,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func buildCommand(p *ProjectOptions, progress *string, backend api.Service) *cobra.Command {
|
||||
func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||
opts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
|
@ -118,7 +117,7 @@ func buildCommand(p *ProjectOptions, progress *string, backend api.Service) *cob
|
|||
cmd.Flags().Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
|
||||
cmd.Flags().MarkHidden("no-rm") //nolint:errcheck
|
||||
cmd.Flags().VarP(&opts.memory, "memory", "m", "Set memory limit for the build container. Not supported by BuildKit.")
|
||||
cmd.Flags().StringVar(progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
|
||||
cmd.Flags().StringVar(&p.Progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of ui output (%s)`, strings.Join(printerModes, ", ")))
|
||||
cmd.Flags().MarkHidden("progress") //nolint:errcheck
|
||||
|
||||
return cmd
|
||||
|
@ -130,6 +129,10 @@ func runBuild(ctx context.Context, backend api.Service, opts buildOptions, servi
|
|||
return err
|
||||
}
|
||||
|
||||
if err := applyPlatforms(project, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiBuildOptions, err := opts.toAPIBuildOptions(services)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -26,8 +26,9 @@ import (
|
|||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/compose-spec/compose-go/dotenv"
|
||||
buildx "github.com/docker/buildx/util/progress"
|
||||
|
||||
"github.com/compose-spec/compose-go/dotenv"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/remote"
|
||||
|
||||
|
@ -117,6 +118,7 @@ type ProjectOptions struct {
|
|||
ProjectDir string
|
||||
EnvFiles []string
|
||||
Compatibility bool
|
||||
Progress string
|
||||
}
|
||||
|
||||
// ProjectFunc does stuff within a types.Project
|
||||
|
@ -170,6 +172,7 @@ func (o *ProjectOptions) addProjectFlags(f *pflag.FlagSet) {
|
|||
f.StringVar(&o.ProjectDir, "project-directory", "", "Specify an alternate working directory\n(default: the path of the, first specified, Compose file)")
|
||||
f.StringVar(&o.WorkDir, "workdir", "", "DEPRECATED! USE --project-directory INSTEAD.\nSpecify an alternate working directory\n(default: the path of the, first specified, Compose file)")
|
||||
f.BoolVar(&o.Compatibility, "compatibility", false, "Run compose in backward compatibility mode")
|
||||
f.StringVar(&o.Progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
_ = f.MarkHidden("workdir")
|
||||
}
|
||||
|
||||
|
@ -294,7 +297,6 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
|||
version bool
|
||||
parallel int
|
||||
dryRun bool
|
||||
progress string
|
||||
)
|
||||
c := &cobra.Command{
|
||||
Short: "Docker Compose",
|
||||
|
@ -359,7 +361,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
|||
ui.Mode = ui.ModeTTY
|
||||
}
|
||||
|
||||
switch progress {
|
||||
switch opts.Progress {
|
||||
case ui.ModeAuto:
|
||||
ui.Mode = ui.ModeAuto
|
||||
case ui.ModeTTY:
|
||||
|
@ -375,7 +377,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
|||
case ui.ModeQuiet, "none":
|
||||
ui.Mode = ui.ModeQuiet
|
||||
default:
|
||||
return fmt.Errorf("unsupported --progress value %q", progress)
|
||||
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
|
||||
}
|
||||
|
||||
if opts.WorkDir != "" {
|
||||
|
@ -446,7 +448,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
|||
portCommand(&opts, dockerCli, backend),
|
||||
imagesCommand(&opts, dockerCli, backend),
|
||||
versionCommand(dockerCli),
|
||||
buildCommand(&opts, &progress, backend),
|
||||
buildCommand(&opts, backend),
|
||||
pushCommand(&opts, backend),
|
||||
pullCommand(&opts, backend),
|
||||
createCommand(&opts, backend),
|
||||
|
@ -478,8 +480,6 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
|||
completeProfileNames(&opts),
|
||||
)
|
||||
|
||||
c.Flags().StringVar(&progress, "progress", buildx.PrinterModeAuto, fmt.Sprintf(`Set type of progress output (%s)`, strings.Join(printerModes, ", ")))
|
||||
|
||||
c.Flags().StringVar(&ansi, "ansi", "auto", `Control when to print ANSI control characters ("never"|"always"|"auto")`)
|
||||
c.Flags().IntVar(¶llel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
|
||||
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
|
||||
|
|
|
@ -123,6 +123,9 @@ func (opts createOptions) Apply(project *types.Project) error {
|
|||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
// N.B. opts.Build means "force build all", but images can still be built
|
||||
// when this is false
|
||||
// e.g. if a service has pull_policy: build or its local image is missing
|
||||
if opts.Build {
|
||||
for i, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
|
@ -132,6 +135,7 @@ func (opts createOptions) Apply(project *types.Project) error {
|
|||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
// opts.noBuild, however, means do not perform ANY builds
|
||||
if opts.noBuild {
|
||||
for i, service := range project.Services {
|
||||
service.Build = nil
|
||||
|
@ -141,6 +145,11 @@ func (opts createOptions) Apply(project *types.Project) error {
|
|||
project.Services[i] = service
|
||||
}
|
||||
}
|
||||
|
||||
if err := applyPlatforms(project, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, scale := range opts.scale {
|
||||
split := strings.Split(scale, "=")
|
||||
if len(split) != 2 {
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
)
|
||||
|
||||
func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error {
|
||||
defaultPlatform := project.Environment["DOCKER_DEFAULT_PLATFORM"]
|
||||
for i := range project.Services {
|
||||
// mutable reference so platform fields can be updated
|
||||
service := &project.Services[i]
|
||||
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// default platform only applies if the service doesn't specify
|
||||
if defaultPlatform != "" && service.Platform == "" {
|
||||
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, defaultPlatform) {
|
||||
return fmt.Errorf("service %q build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: %s", service.Name, defaultPlatform)
|
||||
}
|
||||
service.Platform = defaultPlatform
|
||||
}
|
||||
|
||||
if service.Platform != "" {
|
||||
if len(service.Build.Platforms) > 0 {
|
||||
if !utils.StringContains(service.Build.Platforms, service.Platform) {
|
||||
return fmt.Errorf("service %q build configuration does not support platform: %s", service.Name, service.Platform)
|
||||
}
|
||||
}
|
||||
|
||||
if buildForSinglePlatform || len(service.Build.Platforms) == 0 {
|
||||
// if we're building for a single platform, we want to build for the platform we'll use to run the image
|
||||
// similarly, if no build platforms were explicitly specified, it makes sense to build for the platform
|
||||
// the image is designed for rather than allowing the builder to infer the platform
|
||||
service.Build.Platforms = []string{service.Platform}
|
||||
}
|
||||
}
|
||||
|
||||
// services can specify that they should be built for multiple platforms, which can be used
|
||||
// with `docker compose build` to produce a multi-arch image
|
||||
// other cases, such as `up` and `run`, need a single architecture to actually run
|
||||
// if there is only a single platform present (which might have been inferred
|
||||
// from service.Platform above), it will be used, even if it requires emulation.
|
||||
// if there's more than one platform, then the list is cleared so that the builder
|
||||
// can decide.
|
||||
// TODO(milas): there's no validation that the platform the builder will pick is actually one
|
||||
// of the supported platforms from the build definition
|
||||
// e.g. `build.platforms: [linux/arm64, linux/amd64]` on a `linux/ppc64` machine would build
|
||||
// for `linux/ppc64` instead of returning an error that it's not a valid platform for the service.
|
||||
if buildForSinglePlatform && len(service.Build.Platforms) > 1 {
|
||||
// empty indicates that the builder gets to decide
|
||||
service.Build.Platforms = nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
Copyright 2023 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestApplyPlatforms_InferFromRuntime(t *testing.T) {
|
||||
makeProject := func() *types.Project {
|
||||
return &types.Project{
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"alice/32",
|
||||
},
|
||||
},
|
||||
Platform: "alice/32",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SinglePlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, true))
|
||||
require.EqualValues(t, []string{"alice/32"}, project.Services[0].Build.Platforms)
|
||||
})
|
||||
|
||||
t.Run("MultiPlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, false))
|
||||
require.EqualValues(t, []string{"linux/amd64", "linux/arm64", "alice/32"},
|
||||
project.Services[0].Build.Platforms)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyPlatforms_DockerDefaultPlatform(t *testing.T) {
|
||||
makeProject := func() *types.Project {
|
||||
return &types.Project{
|
||||
Environment: map[string]string{
|
||||
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
|
||||
},
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SinglePlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, true))
|
||||
require.EqualValues(t, []string{"linux/amd64"}, project.Services[0].Build.Platforms)
|
||||
})
|
||||
|
||||
t.Run("MultiPlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.NoError(t, applyPlatforms(project, false))
|
||||
require.EqualValues(t, []string{"linux/amd64", "linux/arm64"},
|
||||
project.Services[0].Build.Platforms)
|
||||
})
|
||||
}
|
||||
|
||||
func TestApplyPlatforms_UnsupportedPlatform(t *testing.T) {
|
||||
makeProject := func() *types.Project {
|
||||
return &types.Project{
|
||||
Environment: map[string]string{
|
||||
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
|
||||
},
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("SinglePlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.EqualError(t, applyPlatforms(project, true),
|
||||
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
|
||||
})
|
||||
|
||||
t.Run("MultiPlatform", func(t *testing.T) {
|
||||
project := makeProject()
|
||||
require.EqualError(t, applyPlatforms(project, false),
|
||||
`service "test" build.platforms does not support value set by DOCKER_DEFAULT_PLATFORM: commodore/64`)
|
||||
})
|
||||
}
|
|
@ -21,6 +21,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
xprogress "github.com/docker/buildx/util/progress"
|
||||
|
||||
cgo "github.com/compose-spec/compose-go/cli"
|
||||
"github.com/compose-spec/compose-go/loader"
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
|
@ -118,6 +120,9 @@ func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *co
|
|||
capDrop: opts.NewListOpts(nil),
|
||||
}
|
||||
createOpts := createOptions{}
|
||||
buildOpts := buildOptions{
|
||||
ProjectOptions: p,
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
|
||||
Short: "Run a one-off command on a service.",
|
||||
|
@ -152,8 +157,12 @@ func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *co
|
|||
return err
|
||||
}
|
||||
|
||||
if createOpts.quietPull {
|
||||
buildOpts.Progress = xprogress.PrinterModeQuiet
|
||||
}
|
||||
|
||||
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
||||
return runRun(ctx, backend, project, options, createOpts, streams)
|
||||
return runRun(ctx, backend, project, options, createOpts, buildOpts, streams)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
|
@ -197,7 +206,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
|||
return pflag.NormalizedName(name)
|
||||
}
|
||||
|
||||
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, streams api.Streams) error {
|
||||
func runRun(ctx context.Context, backend api.Service, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, streams api.Streams) error {
|
||||
err := options.apply(project)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -209,7 +218,16 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
|||
}
|
||||
|
||||
err = progress.Run(ctx, func(ctx context.Context) error {
|
||||
return startDependencies(ctx, backend, *project, options.Service, options.ignoreOrphans)
|
||||
var buildForDeps *api.BuildOptions
|
||||
if !createOpts.noBuild {
|
||||
// allow dependencies needing build to be implicitly selected
|
||||
bo, err := buildOpts.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildForDeps = &bo
|
||||
}
|
||||
return startDependencies(ctx, backend, *project, buildForDeps, options.Service, options.ignoreOrphans)
|
||||
}, streams.Err())
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -224,8 +242,20 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
|||
labels[parts[0]] = parts[1]
|
||||
}
|
||||
|
||||
var buildForRun *api.BuildOptions
|
||||
if !createOpts.noBuild {
|
||||
// dependencies have already been started above, so only the service
|
||||
// being run might need to be built at this point
|
||||
bo, err := buildOpts.toAPIBuildOptions([]string{options.Service})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildForRun = &bo
|
||||
}
|
||||
|
||||
// start container and attach to container streams
|
||||
runOpts := api.RunOptions{
|
||||
Build: buildForRun,
|
||||
Name: options.name,
|
||||
Service: options.Service,
|
||||
Command: options.Command,
|
||||
|
@ -264,7 +294,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
|||
return err
|
||||
}
|
||||
|
||||
func startDependencies(ctx context.Context, backend api.Service, project types.Project, requestedServiceName string, ignoreOrphans bool) error {
|
||||
func startDependencies(ctx context.Context, backend api.Service, project types.Project, buildOpts *api.BuildOptions, requestedServiceName string, ignoreOrphans bool) error {
|
||||
dependencies := types.Services{}
|
||||
var requestedService types.ServiceConfig
|
||||
for _, service := range project.Services {
|
||||
|
@ -278,6 +308,7 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P
|
|||
project.Services = dependencies
|
||||
project.DisabledServices = append(project.DisabledServices, requestedService)
|
||||
err := backend.Create(ctx, &project, api.CreateOptions{
|
||||
Build: buildOpts,
|
||||
IgnoreOrphans: ignoreOrphans,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -23,6 +23,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
xprogress "github.com/docker/buildx/util/progress"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"github.com/docker/compose/v2/cmd/formatter"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -74,6 +76,7 @@ func (opts upOptions) apply(project *types.Project, services []string) error {
|
|||
func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
|
||||
up := upOptions{}
|
||||
create := createOptions{}
|
||||
build := buildOptions{ProjectOptions: p}
|
||||
upCmd := &cobra.Command{
|
||||
Use: "up [OPTIONS] [SERVICE...]",
|
||||
Short: "Create and start containers",
|
||||
|
@ -90,7 +93,7 @@ func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cob
|
|||
if len(up.attach) != 0 && up.attachDependencies {
|
||||
return errors.New("cannot combine --attach and --attach-dependencies")
|
||||
}
|
||||
return runUp(ctx, streams, backend, create, up, project, services)
|
||||
return runUp(ctx, streams, backend, create, up, build, project, services)
|
||||
}),
|
||||
ValidArgsFunction: completeServiceNames(p),
|
||||
}
|
||||
|
@ -148,7 +151,16 @@ func validateFlags(up *upOptions, create *createOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, streams api.Streams, backend api.Service, createOptions createOptions, upOptions upOptions, project *types.Project, services []string) error {
|
||||
func runUp(
|
||||
ctx context.Context,
|
||||
streams api.Streams,
|
||||
backend api.Service,
|
||||
createOptions createOptions,
|
||||
upOptions upOptions,
|
||||
buildOptions buildOptions,
|
||||
project *types.Project,
|
||||
services []string,
|
||||
) error {
|
||||
if len(project.Services) == 0 {
|
||||
return fmt.Errorf("no service selected")
|
||||
}
|
||||
|
@ -163,7 +175,26 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
|
|||
return err
|
||||
}
|
||||
|
||||
var build *api.BuildOptions
|
||||
// this check is technically redundant as createOptions::apply()
|
||||
// already removed all the build sections
|
||||
if !createOptions.noBuild {
|
||||
if createOptions.quietPull {
|
||||
buildOptions.Progress = xprogress.PrinterModeQuiet
|
||||
}
|
||||
// BuildOptions here is nested inside CreateOptions, so
|
||||
// no service list is passed, it will implicitly pick all
|
||||
// services being created, which includes any explicitly
|
||||
// specified via "services" arg here as well as deps
|
||||
bo, err := buildOptions.toAPIBuildOptions(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
build = &bo
|
||||
}
|
||||
|
||||
create := api.CreateOptions{
|
||||
Build: build,
|
||||
Services: services,
|
||||
RemoveOrphans: createOptions.removeOrphans,
|
||||
IgnoreOrphans: createOptions.ignoreOrphans,
|
||||
|
|
|
@ -167,6 +167,7 @@ func (o BuildOptions) Apply(project *types.Project) error {
|
|||
|
||||
// CreateOptions group options of the Create API
|
||||
type CreateOptions struct {
|
||||
Build *BuildOptions
|
||||
// Services defines the services user interacts with
|
||||
Services []string
|
||||
// Remove legacy containers for services that are not defined in the project
|
||||
|
@ -302,6 +303,7 @@ type RemoveOptions struct {
|
|||
|
||||
// RunOptions group options of the Run API
|
||||
type RunOptions struct {
|
||||
Build *BuildOptions
|
||||
// Project is the compose project used to define this app. Might be nil if user ran command just with project name
|
||||
Project *types.Project
|
||||
Name string
|
||||
|
|
|
@ -18,6 +18,7 @@ package compose
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -43,7 +44,6 @@ import (
|
|||
"github.com/moby/buildkit/session/sshforward/sshprovider"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
// required to get default driver registered
|
||||
|
@ -56,13 +56,13 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
|
|||
return err
|
||||
}
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
_, err := s.build(ctx, project, options)
|
||||
_, err := s.build(ctx, project, options, nil)
|
||||
return err
|
||||
}, s.stdinfo(), "Building")
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions) (map[string]string, error) {
|
||||
func (s *composeService) build(ctx context.Context, project *types.Project, options api.BuildOptions, localImages map[string]string) (map[string]string, error) {
|
||||
buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -117,6 +117,12 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
|||
return nil
|
||||
}
|
||||
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
_, localImagePresent := localImages[image]
|
||||
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !buildkitEnabled {
|
||||
id, err := s.doBuildClassic(ctx, project, service, options)
|
||||
if err != nil {
|
||||
|
@ -183,7 +189,7 @@ func getServiceIndex(project *types.Project, name string) (types.ServiceConfig,
|
|||
return service, idx
|
||||
}
|
||||
|
||||
func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, quietPull bool) error {
|
||||
func (s *composeService) ensureImagesExists(ctx context.Context, project *types.Project, buildOpts *api.BuildOptions, quietPull bool) error {
|
||||
for _, service := range project.Services {
|
||||
if service.Image == "" && service.Build == nil {
|
||||
return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
|
||||
|
@ -204,22 +210,10 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
|||
return err
|
||||
}
|
||||
|
||||
mode := xprogress.PrinterModeAuto
|
||||
if quietPull {
|
||||
mode = xprogress.PrinterModeQuiet
|
||||
}
|
||||
|
||||
buildRequired, err := s.prepareProjectForBuild(project, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if buildRequired {
|
||||
if buildOpts != nil {
|
||||
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
|
||||
func(ctx context.Context) error {
|
||||
builtImages, err := s.build(ctx, project, api.BuildOptions{
|
||||
Progress: mode,
|
||||
})
|
||||
builtImages, err := s.build(ctx, project, *buildOpts, images)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -249,37 +243,6 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *composeService) prepareProjectForBuild(project *types.Project, images map[string]string) (bool, error) {
|
||||
buildRequired := false
|
||||
err := api.BuildOptions{}.Apply(project)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for i, service := range project.Services {
|
||||
if service.Build == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
image := api.GetImageNameOrDefault(service, project.Name)
|
||||
_, localImagePresent := images[image]
|
||||
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
|
||||
service.Build = nil
|
||||
project.Services[i] = service
|
||||
continue
|
||||
}
|
||||
|
||||
if service.Platform == "" {
|
||||
// let builder to build for default platform
|
||||
service.Build.Platforms = nil
|
||||
} else {
|
||||
service.Build.Platforms = []string{service.Platform}
|
||||
}
|
||||
project.Services[i] = service
|
||||
buildRequired = true
|
||||
}
|
||||
return buildRequired, nil
|
||||
}
|
||||
|
||||
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
|
||||
var imageNames []string
|
||||
for _, s := range project.Services {
|
||||
|
@ -318,8 +281,10 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
|||
Variant: inspect.Variant,
|
||||
}
|
||||
if !platforms.NewMatcher(platform).Match(actual) {
|
||||
return nil, errors.Errorf("image with reference %s was found but does not match the specified platform: wanted %s, actual: %s",
|
||||
imgName, platforms.Format(platform), platforms.Format(actual))
|
||||
// there is a local image, but it's for the wrong platform, so
|
||||
// pretend it doesn't exist so that we can pull/build an image
|
||||
// for the correct platform instead
|
||||
delete(images, imgName)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,7 +327,7 @@ func resolveAndMergeBuildArgs(
|
|||
}
|
||||
|
||||
func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, options api.BuildOptions) (build.Options, error) {
|
||||
plats, err := addPlatforms(project, service)
|
||||
plats, err := parsePlatforms(service)
|
||||
if err != nil {
|
||||
return build.Options{}, err
|
||||
}
|
||||
|
@ -515,24 +480,6 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess
|
|||
return secretsprovider.NewSecretProvider(store), nil
|
||||
}
|
||||
|
||||
func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
|
||||
plats, err := useDockerDefaultOrServicePlatform(project, service, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, buildPlatform := range service.Build.Platforms {
|
||||
p, err := platforms.Parse(buildPlatform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !utils.Contains(plats, p) {
|
||||
plats = append(plats, p)
|
||||
}
|
||||
}
|
||||
return plats, nil
|
||||
}
|
||||
|
||||
func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
|
||||
ret := make(types.Labels)
|
||||
if service.Build != nil {
|
||||
|
@ -555,37 +502,25 @@ func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedCon
|
|||
return namedContexts
|
||||
}
|
||||
|
||||
func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
|
||||
var plats []specs.Platform
|
||||
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
|
||||
if len(platformList) > 0 && !utils.StringContains(platformList, platform) {
|
||||
return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM %q value should be part of the service.build.platforms: %q", platform, platformList)
|
||||
}
|
||||
p, err := platforms.Parse(platform)
|
||||
func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) {
|
||||
if service.Build == nil || len(service.Build.Platforms) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret := make([]specs.Platform, len(service.Build.Platforms))
|
||||
for i := range service.Build.Platforms {
|
||||
p, err := platforms.Parse(service.Build.Platforms[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
ret[i] = p
|
||||
}
|
||||
plats = append(plats, p)
|
||||
}
|
||||
return plats, nil
|
||||
}
|
||||
|
||||
func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
|
||||
plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
|
||||
if (len(plats) > 0 && useOnePlatform) || err != nil {
|
||||
return plats, err
|
||||
}
|
||||
|
||||
if service.Platform != "" {
|
||||
if len(service.Build.Platforms) > 0 && !utils.StringContains(service.Build.Platforms, service.Platform) {
|
||||
return nil, fmt.Errorf("service.platform %q should be part of the service.build.platforms: %q", service.Platform, service.Build.Platforms)
|
||||
}
|
||||
// User defined a service platform and no build platforms, so we should keep the one define on the service level
|
||||
p, err := platforms.Parse(service.Platform)
|
||||
if !utils.Contains(plats, p) {
|
||||
plats = append(plats, p)
|
||||
}
|
||||
return plats, err
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plats, nil
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package compose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/types"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPrepareProjectForBuild(t *testing.T) {
|
||||
t.Run("build service platform", func(t *testing.T) {
|
||||
project := types.Project{
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
"alice/32",
|
||||
},
|
||||
},
|
||||
Platform: "alice/32",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
_, err := s.prepareProjectForBuild(&project, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"alice/32"})
|
||||
})
|
||||
|
||||
t.Run("build DOCKER_DEFAULT_PLATFORM", func(t *testing.T) {
|
||||
project := types.Project{
|
||||
Environment: map[string]string{
|
||||
"DOCKER_DEFAULT_PLATFORM": "linux/amd64",
|
||||
},
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
_, err := s.prepareProjectForBuild(&project, nil)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, project.Services[0].Build.Platforms, types.StringList{"linux/amd64"})
|
||||
})
|
||||
|
||||
t.Run("skip existing image", func(t *testing.T) {
|
||||
project := types.Project{
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
_, err := s.prepareProjectForBuild(&project, map[string]string{"foo": "exists"})
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, project.Services[0].Build == nil)
|
||||
})
|
||||
|
||||
t.Run("unsupported build platform", func(t *testing.T) {
|
||||
project := types.Project{
|
||||
Environment: map[string]string{
|
||||
"DOCKER_DEFAULT_PLATFORM": "commodore/64",
|
||||
},
|
||||
Services: []types.ServiceConfig{
|
||||
{
|
||||
Name: "test",
|
||||
Image: "foo",
|
||||
Build: &types.BuildConfig{
|
||||
Context: ".",
|
||||
Platforms: []string{
|
||||
"linux/amd64",
|
||||
"linux/arm64",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := &composeService{}
|
||||
_, err := s.prepareProjectForBuild(&project, nil)
|
||||
assert.Check(t, err != nil)
|
||||
})
|
||||
}
|
|
@ -62,9 +62,9 @@ type createConfigs struct {
|
|||
Links []string
|
||||
}
|
||||
|
||||
func (s *composeService) Create(ctx context.Context, project *types.Project, options api.CreateOptions) error {
|
||||
func (s *composeService) Create(ctx context.Context, project *types.Project, createOpts api.CreateOptions) error {
|
||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||
return s.create(ctx, project, options)
|
||||
return s.create(ctx, project, createOpts)
|
||||
}, s.stdinfo(), "Creating")
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
|||
return err
|
||||
}
|
||||
|
||||
err = s.ensureImagesExists(ctx, project, options.QuietPull)
|
||||
err = s.ensureImagesExists(ctx, project, options.Build, options.QuietPull)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
|||
Add(api.SlugLabel, slug).
|
||||
Add(api.OneoffLabel, "True")
|
||||
|
||||
if err := s.ensureImagesExists(ctx, project, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
|
||||
if err := s.ensureImagesExists(ctx, project, opts.Build, opts.QuietPull); err != nil { // all dependencies already checked, but might miss service img
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
|
|
@ -85,10 +85,6 @@ func (s *composeService) getSyncImplementation(project *types.Project) sync.Sync
|
|||
}
|
||||
|
||||
func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, _ api.WatchOptions) error { //nolint: gocyclo
|
||||
_, err := s.prepareProjectForBuild(project, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := project.ForServices(services); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -458,6 +454,12 @@ func (s *composeService) handleWatchBatch(
|
|||
)
|
||||
err := s.Up(ctx, project, api.UpOptions{
|
||||
Create: api.CreateOptions{
|
||||
Build: &api.BuildOptions{
|
||||
Pull: false,
|
||||
Push: false,
|
||||
// restrict the build to ONLY this service, not any of its dependencies
|
||||
Services: []string{serviceName},
|
||||
},
|
||||
Services: []string{serviceName},
|
||||
Inherit: true,
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue