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 {
|
type buildOptions struct {
|
||||||
*ProjectOptions
|
*ProjectOptions
|
||||||
composeOptions
|
|
||||||
quiet bool
|
quiet bool
|
||||||
pull bool
|
pull bool
|
||||||
push bool
|
push bool
|
||||||
|
@ -73,7 +72,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCommand(p *ProjectOptions, progress *string, backend api.Service) *cobra.Command {
|
func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command {
|
||||||
opts := buildOptions{
|
opts := buildOptions{
|
||||||
ProjectOptions: p,
|
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().Bool("no-rm", false, "Do not remove intermediate containers after a successful build. DEPRECATED")
|
||||||
cmd.Flags().MarkHidden("no-rm") //nolint:errcheck
|
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().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
|
cmd.Flags().MarkHidden("progress") //nolint:errcheck
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -130,6 +129,10 @@ func runBuild(ctx context.Context, backend api.Service, opts buildOptions, servi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := applyPlatforms(project, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
apiBuildOptions, err := opts.toAPIBuildOptions(services)
|
apiBuildOptions, err := opts.toAPIBuildOptions(services)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -26,8 +26,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/dotenv"
|
|
||||||
buildx "github.com/docker/buildx/util/progress"
|
buildx "github.com/docker/buildx/util/progress"
|
||||||
|
|
||||||
|
"github.com/compose-spec/compose-go/dotenv"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/compose/v2/pkg/remote"
|
"github.com/docker/compose/v2/pkg/remote"
|
||||||
|
|
||||||
|
@ -117,6 +118,7 @@ type ProjectOptions struct {
|
||||||
ProjectDir string
|
ProjectDir string
|
||||||
EnvFiles []string
|
EnvFiles []string
|
||||||
Compatibility bool
|
Compatibility bool
|
||||||
|
Progress string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectFunc does stuff within a types.Project
|
// 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.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.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.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")
|
_ = f.MarkHidden("workdir")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,7 +297,6 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
||||||
version bool
|
version bool
|
||||||
parallel int
|
parallel int
|
||||||
dryRun bool
|
dryRun bool
|
||||||
progress string
|
|
||||||
)
|
)
|
||||||
c := &cobra.Command{
|
c := &cobra.Command{
|
||||||
Short: "Docker Compose",
|
Short: "Docker Compose",
|
||||||
|
@ -359,7 +361,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
||||||
ui.Mode = ui.ModeTTY
|
ui.Mode = ui.ModeTTY
|
||||||
}
|
}
|
||||||
|
|
||||||
switch progress {
|
switch opts.Progress {
|
||||||
case ui.ModeAuto:
|
case ui.ModeAuto:
|
||||||
ui.Mode = ui.ModeAuto
|
ui.Mode = ui.ModeAuto
|
||||||
case ui.ModeTTY:
|
case ui.ModeTTY:
|
||||||
|
@ -375,7 +377,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
||||||
case ui.ModeQuiet, "none":
|
case ui.ModeQuiet, "none":
|
||||||
ui.Mode = ui.ModeQuiet
|
ui.Mode = ui.ModeQuiet
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported --progress value %q", progress)
|
return fmt.Errorf("unsupported --progress value %q", opts.Progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.WorkDir != "" {
|
if opts.WorkDir != "" {
|
||||||
|
@ -446,7 +448,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
||||||
portCommand(&opts, dockerCli, backend),
|
portCommand(&opts, dockerCli, backend),
|
||||||
imagesCommand(&opts, dockerCli, backend),
|
imagesCommand(&opts, dockerCli, backend),
|
||||||
versionCommand(dockerCli),
|
versionCommand(dockerCli),
|
||||||
buildCommand(&opts, &progress, backend),
|
buildCommand(&opts, backend),
|
||||||
pushCommand(&opts, backend),
|
pushCommand(&opts, backend),
|
||||||
pullCommand(&opts, backend),
|
pullCommand(&opts, backend),
|
||||||
createCommand(&opts, backend),
|
createCommand(&opts, backend),
|
||||||
|
@ -478,8 +480,6 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //
|
||||||
completeProfileNames(&opts),
|
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().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().IntVar(¶llel, "parallel", -1, `Control max parallelism, -1 for unlimited`)
|
||||||
c.Flags().BoolVarP(&version, "version", "v", false, "Show the Docker Compose version information")
|
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
|
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 {
|
if opts.Build {
|
||||||
for i, service := range project.Services {
|
for i, service := range project.Services {
|
||||||
if service.Build == nil {
|
if service.Build == nil {
|
||||||
|
@ -132,6 +135,7 @@ func (opts createOptions) Apply(project *types.Project) error {
|
||||||
project.Services[i] = service
|
project.Services[i] = service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// opts.noBuild, however, means do not perform ANY builds
|
||||||
if opts.noBuild {
|
if opts.noBuild {
|
||||||
for i, service := range project.Services {
|
for i, service := range project.Services {
|
||||||
service.Build = nil
|
service.Build = nil
|
||||||
|
@ -141,6 +145,11 @@ func (opts createOptions) Apply(project *types.Project) error {
|
||||||
project.Services[i] = service
|
project.Services[i] = service
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := applyPlatforms(project, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, scale := range opts.scale {
|
for _, scale := range opts.scale {
|
||||||
split := strings.Split(scale, "=")
|
split := strings.Split(scale, "=")
|
||||||
if len(split) != 2 {
|
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"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
xprogress "github.com/docker/buildx/util/progress"
|
||||||
|
|
||||||
cgo "github.com/compose-spec/compose-go/cli"
|
cgo "github.com/compose-spec/compose-go/cli"
|
||||||
"github.com/compose-spec/compose-go/loader"
|
"github.com/compose-spec/compose-go/loader"
|
||||||
"github.com/compose-spec/compose-go/types"
|
"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),
|
capDrop: opts.NewListOpts(nil),
|
||||||
}
|
}
|
||||||
createOpts := createOptions{}
|
createOpts := createOptions{}
|
||||||
|
buildOpts := buildOptions{
|
||||||
|
ProjectOptions: p,
|
||||||
|
}
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
|
Use: "run [OPTIONS] SERVICE [COMMAND] [ARGS...]",
|
||||||
Short: "Run a one-off command on a service.",
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if createOpts.quietPull {
|
||||||
|
buildOpts.Progress = xprogress.PrinterModeQuiet
|
||||||
|
}
|
||||||
|
|
||||||
options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
|
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),
|
ValidArgsFunction: completeServiceNames(p),
|
||||||
}
|
}
|
||||||
|
@ -197,7 +206,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
|
||||||
return pflag.NormalizedName(name)
|
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)
|
err := options.apply(project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
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())
|
}, streams.Err())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -224,8 +242,20 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||||
labels[parts[0]] = parts[1]
|
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
|
// start container and attach to container streams
|
||||||
runOpts := api.RunOptions{
|
runOpts := api.RunOptions{
|
||||||
|
Build: buildForRun,
|
||||||
Name: options.name,
|
Name: options.name,
|
||||||
Service: options.Service,
|
Service: options.Service,
|
||||||
Command: options.Command,
|
Command: options.Command,
|
||||||
|
@ -264,7 +294,7 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
|
||||||
return err
|
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{}
|
dependencies := types.Services{}
|
||||||
var requestedService types.ServiceConfig
|
var requestedService types.ServiceConfig
|
||||||
for _, service := range project.Services {
|
for _, service := range project.Services {
|
||||||
|
@ -278,6 +308,7 @@ func startDependencies(ctx context.Context, backend api.Service, project types.P
|
||||||
project.Services = dependencies
|
project.Services = dependencies
|
||||||
project.DisabledServices = append(project.DisabledServices, requestedService)
|
project.DisabledServices = append(project.DisabledServices, requestedService)
|
||||||
err := backend.Create(ctx, &project, api.CreateOptions{
|
err := backend.Create(ctx, &project, api.CreateOptions{
|
||||||
|
Build: buildOpts,
|
||||||
IgnoreOrphans: ignoreOrphans,
|
IgnoreOrphans: ignoreOrphans,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
xprogress "github.com/docker/buildx/util/progress"
|
||||||
|
|
||||||
"github.com/compose-spec/compose-go/types"
|
"github.com/compose-spec/compose-go/types"
|
||||||
"github.com/docker/compose/v2/cmd/formatter"
|
"github.com/docker/compose/v2/cmd/formatter"
|
||||||
"github.com/spf13/cobra"
|
"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 {
|
func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command {
|
||||||
up := upOptions{}
|
up := upOptions{}
|
||||||
create := createOptions{}
|
create := createOptions{}
|
||||||
|
build := buildOptions{ProjectOptions: p}
|
||||||
upCmd := &cobra.Command{
|
upCmd := &cobra.Command{
|
||||||
Use: "up [OPTIONS] [SERVICE...]",
|
Use: "up [OPTIONS] [SERVICE...]",
|
||||||
Short: "Create and start containers",
|
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 {
|
if len(up.attach) != 0 && up.attachDependencies {
|
||||||
return errors.New("cannot combine --attach and --attach-dependencies")
|
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),
|
ValidArgsFunction: completeServiceNames(p),
|
||||||
}
|
}
|
||||||
|
@ -148,7 +151,16 @@ func validateFlags(up *upOptions, create *createOptions) error {
|
||||||
return nil
|
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 {
|
if len(project.Services) == 0 {
|
||||||
return fmt.Errorf("no service selected")
|
return fmt.Errorf("no service selected")
|
||||||
}
|
}
|
||||||
|
@ -163,7 +175,26 @@ func runUp(ctx context.Context, streams api.Streams, backend api.Service, create
|
||||||
return err
|
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{
|
create := api.CreateOptions{
|
||||||
|
Build: build,
|
||||||
Services: services,
|
Services: services,
|
||||||
RemoveOrphans: createOptions.removeOrphans,
|
RemoveOrphans: createOptions.removeOrphans,
|
||||||
IgnoreOrphans: createOptions.ignoreOrphans,
|
IgnoreOrphans: createOptions.ignoreOrphans,
|
||||||
|
|
|
@ -167,6 +167,7 @@ func (o BuildOptions) Apply(project *types.Project) error {
|
||||||
|
|
||||||
// CreateOptions group options of the Create API
|
// CreateOptions group options of the Create API
|
||||||
type CreateOptions struct {
|
type CreateOptions struct {
|
||||||
|
Build *BuildOptions
|
||||||
// Services defines the services user interacts with
|
// Services defines the services user interacts with
|
||||||
Services []string
|
Services []string
|
||||||
// Remove legacy containers for services that are not defined in the project
|
// 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
|
// RunOptions group options of the Run API
|
||||||
type RunOptions struct {
|
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 is the compose project used to define this app. Might be nil if user ran command just with project name
|
||||||
Project *types.Project
|
Project *types.Project
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -18,6 +18,7 @@ package compose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -43,7 +44,6 @@ import (
|
||||||
"github.com/moby/buildkit/session/sshforward/sshprovider"
|
"github.com/moby/buildkit/session/sshforward/sshprovider"
|
||||||
"github.com/moby/buildkit/util/entitlements"
|
"github.com/moby/buildkit/util/entitlements"
|
||||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
// required to get default driver registered
|
// required to get default driver registered
|
||||||
|
@ -56,13 +56,13 @@ func (s *composeService) Build(ctx context.Context, project *types.Project, opti
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
_, err := s.build(ctx, project, options)
|
_, err := s.build(ctx, project, options, nil)
|
||||||
return err
|
return err
|
||||||
}, s.stdinfo(), "Building")
|
}, s.stdinfo(), "Building")
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gocyclo
|
//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()
|
buildkitEnabled, err := s.dockerCli.BuildKitEnabled()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -117,6 +117,12 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
image := api.GetImageNameOrDefault(service, project.Name)
|
||||||
|
_, localImagePresent := localImages[image]
|
||||||
|
if localImagePresent && service.PullPolicy != types.PullPolicyBuild {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !buildkitEnabled {
|
if !buildkitEnabled {
|
||||||
id, err := s.doBuildClassic(ctx, project, service, options)
|
id, err := s.doBuildClassic(ctx, project, service, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -183,7 +189,7 @@ func getServiceIndex(project *types.Project, name string) (types.ServiceConfig,
|
||||||
return service, idx
|
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 {
|
for _, service := range project.Services {
|
||||||
if service.Image == "" && service.Build == nil {
|
if service.Image == "" && service.Build == nil {
|
||||||
return fmt.Errorf("invalid service %q. Must specify either image or build", service.Name)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
mode := xprogress.PrinterModeAuto
|
if buildOpts != nil {
|
||||||
if quietPull {
|
|
||||||
mode = xprogress.PrinterModeQuiet
|
|
||||||
}
|
|
||||||
|
|
||||||
buildRequired, err := s.prepareProjectForBuild(project, images)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if buildRequired {
|
|
||||||
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
|
err = tracing.SpanWrapFunc("project/build", tracing.ProjectOptions(project),
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
builtImages, err := s.build(ctx, project, api.BuildOptions{
|
builtImages, err := s.build(ctx, project, *buildOpts, images)
|
||||||
Progress: mode,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -249,37 +243,6 @@ func (s *composeService) ensureImagesExists(ctx context.Context, project *types.
|
||||||
return nil
|
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) {
|
func (s *composeService) getLocalImagesDigests(ctx context.Context, project *types.Project) (map[string]string, error) {
|
||||||
var imageNames []string
|
var imageNames []string
|
||||||
for _, s := range project.Services {
|
for _, s := range project.Services {
|
||||||
|
@ -318,8 +281,10 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
|
||||||
Variant: inspect.Variant,
|
Variant: inspect.Variant,
|
||||||
}
|
}
|
||||||
if !platforms.NewMatcher(platform).Match(actual) {
|
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",
|
// there is a local image, but it's for the wrong platform, so
|
||||||
imgName, platforms.Format(platform), platforms.Format(actual))
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return build.Options{}, err
|
return build.Options{}, err
|
||||||
}
|
}
|
||||||
|
@ -515,24 +480,6 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess
|
||||||
return secretsprovider.NewSecretProvider(store), nil
|
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 {
|
func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
|
||||||
ret := make(types.Labels)
|
ret := make(types.Labels)
|
||||||
if service.Build != nil {
|
if service.Build != nil {
|
||||||
|
@ -555,37 +502,25 @@ func toBuildContexts(additionalContexts types.Mapping) map[string]build.NamedCon
|
||||||
return namedContexts
|
return namedContexts
|
||||||
}
|
}
|
||||||
|
|
||||||
func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
|
func parsePlatforms(service types.ServiceConfig) ([]specs.Platform, error) {
|
||||||
var plats []specs.Platform
|
if service.Build == nil || len(service.Build.Platforms) == 0 {
|
||||||
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
|
return nil, nil
|
||||||
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)
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
} else {
|
||||||
|
ret[i] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := errors.Join(errs...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
plats = append(plats, p)
|
|
||||||
}
|
|
||||||
return plats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func useDockerDefaultOrServicePlatform(project *types.Project, service types.ServiceConfig, useOnePlatform bool) ([]specs.Platform, error) {
|
return ret, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
return plats, 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
|
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 progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
return s.create(ctx, project, options)
|
return s.create(ctx, project, createOpts)
|
||||||
}, s.stdinfo(), "Creating")
|
}, s.stdinfo(), "Creating")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.ensureImagesExists(ctx, project, options.QuietPull)
|
err = s.ensureImagesExists(ctx, project, options.Build, options.QuietPull)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ func (s *composeService) prepareRun(ctx context.Context, project *types.Project,
|
||||||
Add(api.SlugLabel, slug).
|
Add(api.SlugLabel, slug).
|
||||||
Add(api.OneoffLabel, "True")
|
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
|
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
|
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 {
|
if err := project.ForServices(services); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -458,6 +454,12 @@ func (s *composeService) handleWatchBatch(
|
||||||
)
|
)
|
||||||
err := s.Up(ctx, project, api.UpOptions{
|
err := s.Up(ctx, project, api.UpOptions{
|
||||||
Create: api.CreateOptions{
|
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},
|
Services: []string{serviceName},
|
||||||
Inherit: true,
|
Inherit: true,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue