diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 6282e7a86..2583039c1 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -83,10 +83,8 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti } if len(buildOptions.Platforms) > 1 { buildOptions.Exports = []bclient.ExportEntry{{ - Type: "image", - Attrs: map[string]string{ - "push": "true", - }, + Type: "image", + Attrs: map[string]string{}, }} } opts[imageName] = buildOptions @@ -177,7 +175,9 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri "load": "true", }, }} - opt.Platforms = []specs.Platform{} + if opt.Platforms, err = useDockerDefaultPlatform(project, service.Build.Platforms); err != nil { + opt.Platforms = []specs.Platform{} + } } opts[imageName] = opt continue @@ -360,14 +360,11 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess } func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) { - var plats []specs.Platform - if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok { - p, err := platforms.Parse(platform) - if err != nil { - return nil, err - } - plats = append(plats, p) + plats, err := useDockerDefaultPlatform(project, service.Build.Platforms) + if err != nil { + return nil, err } + if service.Platform != "" && !utils.StringContains(service.Build.Platforms, service.Platform) { return nil, fmt.Errorf("service.platform should be part of the service.build.platforms: %q", service.Platform) } @@ -377,6 +374,23 @@ func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs. if err != nil { return nil, err } + if !utils.Contains(plats, p) { + plats = append(plats, p) + } + } + return plats, nil +} + +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 !utils.StringContains(platformList, platform) { + return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM value should be part of the service.build.platforms: %q", platform) + } + p, err := platforms.Parse(platform) + if err != nil { + return nil, err + } plats = append(plats, p) } return plats, nil diff --git a/pkg/compose/build_buildkit.go b/pkg/compose/build_buildkit.go index b11251ec2..912c520c2 100644 --- a/pkg/compose/build_buildkit.go +++ b/pkg/compose/build_buildkit.go @@ -102,9 +102,10 @@ func (s *composeService) getDrivers(ctx context.Context) ([]build.DriverInfo, er continue } } - f = driver.GetFactory(ng.Driver, true) if f == nil { - return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver) + if f = driver.GetFactory(ng.Driver, true); f == nil { + return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver) + } } } else { ep := ng.Nodes[0].Endpoint diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index b91d42b61..7eeebf948 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -248,19 +248,12 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { c := NewParallelCLI(t) // declare builder - result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap", "--driver-opt", - "network=host", "--buildkitd-flags", "--allow-insecure-entitlement network.host") - assert.NilError(t, result.Error) - - // start local registry - result = c.RunDockerCmd(t, "run", "-d", "-p", "5001:5000", "--restart=always", - "--name", "registry", "registry:2") + result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap") assert.NilError(t, result.Error) t.Cleanup(func() { c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down") _ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform") - _ = c.RunDockerCmd(t, "rm", "-f", "registry") }) t.Run("platform not supported by builder", func(t *testing.T) { @@ -275,9 +268,8 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { t.Run("multi-arch build ok", func(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build") assert.NilError(t, res.Error, res.Stderr()) - res = c.RunDockerCmd(t, "manifest", "inspect", "--insecure", "localhost:5001/build-test-platform:test") - res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) - res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) + res.Assert(t, icmd.Expected{Out: "I am building for linux/arm64"}) + res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"}) }) @@ -285,16 +277,12 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "-f", "fixtures/build-test/platforms/compose-multiple-platform-builds.yaml", "build") assert.NilError(t, res.Error, res.Stderr()) - res = c.RunDockerCmd(t, "manifest", "inspect", "--insecure", "localhost:5001/build-test-platform-a:test") - res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) - res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) - res = c.RunDockerCmd(t, "manifest", "inspect", "--insecure", "localhost:5001/build-test-platform-b:test") - res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) - res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) - res = c.RunDockerCmd(t, "manifest", "inspect", "--insecure", "localhost:5001/build-test-platform-c:test") - res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) - res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) - + res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/arm64"}) + res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/amd64"}) + res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/arm64"}) + res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/amd64"}) + res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/arm64"}) + res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/amd64"}) }) t.Run("multi-arch up --build", func(t *testing.T) { @@ -302,6 +290,16 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { assert.NilError(t, res.Error, res.Stderr()) res.Assert(t, icmd.Expected{Out: "platforms-platforms-1 exited with code 0"}) }) + + t.Run("use DOCKER_DEFAULT_PLATFORM value when up --build", func(t *testing.T) { + cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build") + res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=linux/amd64") + }) + assert.NilError(t, res.Error, res.Stderr()) + res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"}) + assert.Assert(t, !strings.Contains(res.Stdout(), "I am building for linux/arm64")) + }) } func TestBuildPlatformsStandardErrors(t *testing.T) { @@ -335,4 +333,15 @@ func TestBuildPlatformsStandardErrors(t *testing.T) { Err: `service.platform should be part of the service.build.platforms: "linux/riscv64"`, }) }) + + t.Run("DOCKER_DEFAULT_PLATFORM value not defined in platforms build section", func(t *testing.T) { + cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build") + res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) { + cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=windows/amd64") + }) + res.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: `DOCKER_DEFAULT_PLATFORM value should be part of the service.build.platforms: "windows/amd64"`, + }) + }) } diff --git a/pkg/e2e/fixtures/build-test/platforms/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/Dockerfile index 643926d8b..ef22c17f6 100644 --- a/pkg/e2e/fixtures/build-test/platforms/Dockerfile +++ b/pkg/e2e/fixtures/build-test/platforms/Dockerfile @@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build ARG TARGETPLATFORM ARG BUILDPLATFORM -RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log +RUN echo "I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log FROM alpine COPY --from=build /log /log diff --git a/pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml b/pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml index 0f8ce9936..aac3a3db9 100644 --- a/pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml +++ b/pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml @@ -1,20 +1,20 @@ services: serviceA: - image: localhost:5001/build-test-platform-a:test + image: build-test-platform-a:test build: context: ./contextServiceA platforms: - linux/amd64 - linux/arm64 serviceB: - image: localhost:5001/build-test-platform-b:test + image: build-test-platform-b:test build: context: ./contextServiceB platforms: - linux/amd64 - linux/arm64 serviceC: - image: localhost:5001/build-test-platform-c:test + image: build-test-platform-c:test build: context: ./contextServiceC platforms: diff --git a/pkg/e2e/fixtures/build-test/platforms/compose.yaml b/pkg/e2e/fixtures/build-test/platforms/compose.yaml index e8ba350ab..73421f479 100644 --- a/pkg/e2e/fixtures/build-test/platforms/compose.yaml +++ b/pkg/e2e/fixtures/build-test/platforms/compose.yaml @@ -1,6 +1,6 @@ services: platforms: - image: localhost:5001/build-test-platform:test + image: build-test-platform:test build: context: . platforms: diff --git a/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile index 057ed864c..468b2b10d 100644 --- a/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile +++ b/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile @@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build ARG TARGETPLATFORM ARG BUILDPLATFORM -RUN echo "I'm Service A and I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log +RUN echo "I'm Service A and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log FROM alpine COPY --from=build /log /log diff --git a/pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile index 88eecb902..cfa2ae34a 100644 --- a/pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile +++ b/pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile @@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build ARG TARGETPLATFORM ARG BUILDPLATFORM -RUN echo "I'm Service B and I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log +RUN echo "I'm Service B and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log FROM alpine COPY --from=build /log /log diff --git a/pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile index 1b9172994..3216f6182 100644 --- a/pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile +++ b/pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile @@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build ARG TARGETPLATFORM ARG BUILDPLATFORM -RUN echo "I'm Service C and I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log +RUN echo "I'm Service C and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log FROM alpine COPY --from=build /log /log diff --git a/pkg/utils/slices.go b/pkg/utils/slices.go new file mode 100644 index 000000000..3b635c25d --- /dev/null +++ b/pkg/utils/slices.go @@ -0,0 +1,30 @@ +/* + 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 utils + +import "reflect" + +// Contains helps to detect if a non-comparable struct is part of an array +// only use this method if you can't rely on existing golang Contains function of slices (https://pkg.go.dev/golang.org/x/exp/slices#Contains) +func Contains[T any](origin []T, element T) bool { + for _, v := range origin { + if reflect.DeepEqual(v, element) { + return true + } + } + return false +} diff --git a/pkg/utils/slices_test.go b/pkg/utils/slices_test.go new file mode 100644 index 000000000..d9468afef --- /dev/null +++ b/pkg/utils/slices_test.go @@ -0,0 +1,95 @@ +/* + 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 utils + +import ( + "testing" + + specs "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestContains(t *testing.T) { + source := []specs.Platform{ + { + Architecture: "linux/amd64", + OS: "darwin", + OSVersion: "", + OSFeatures: nil, + Variant: "", + }, + { + Architecture: "linux/arm64", + OS: "linux", + OSVersion: "12", + OSFeatures: nil, + Variant: "v8", + }, + { + Architecture: "", + OS: "", + OSVersion: "", + OSFeatures: nil, + Variant: "", + }, + } + + type args struct { + origin []specs.Platform + element specs.Platform + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "element found", + args: args{ + origin: source, + element: specs.Platform{ + Architecture: "linux/arm64", + OS: "linux", + OSVersion: "12", + OSFeatures: nil, + Variant: "v8", + }, + }, + want: true, + }, + { + name: "element not found", + args: args{ + origin: source, + element: specs.Platform{ + Architecture: "linux/arm64", + OS: "darwin", + OSVersion: "12", + OSFeatures: nil, + Variant: "v8", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Contains(tt.args.origin, tt.args.element); got != tt.want { + t.Errorf("Contains() = %v, want %v", got, tt.want) + } + }) + } +}