don't push images at the end of multi-arch build (and simplify e2e tests)

support DOCKER_DEFAULT_PLATFORM when 'compose up --build'
add tests to check behaviour when DOCKER_DEFAULT_PLATFORM is defined

Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com>
This commit is contained in:
Guillaume Lours 2022-08-31 20:53:41 +02:00
parent 8ed2d8ad07
commit e016faac33
11 changed files with 192 additions and 43 deletions

View File

@ -84,9 +84,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
if len(buildOptions.Platforms) > 1 { if len(buildOptions.Platforms) > 1 {
buildOptions.Exports = []bclient.ExportEntry{{ buildOptions.Exports = []bclient.ExportEntry{{
Type: "image", Type: "image",
Attrs: map[string]string{ Attrs: map[string]string{},
"push": "true",
},
}} }}
} }
opts[imageName] = buildOptions opts[imageName] = buildOptions
@ -177,8 +175,10 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
"load": "true", "load": "true",
}, },
}} }}
if opt.Platforms, err = useDockerDefaultPlatform(project, service.Build.Platforms); err != nil {
opt.Platforms = []specs.Platform{} opt.Platforms = []specs.Platform{}
} }
}
opts[imageName] = opt opts[imageName] = opt
continue 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) { func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
var plats []specs.Platform plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
p, err := platforms.Parse(platform)
if err != nil { if err != nil {
return nil, err return nil, err
} }
plats = append(plats, p)
}
if service.Platform != "" && !utils.StringContains(service.Build.Platforms, service.Platform) { 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) 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 { if err != nil {
return nil, err 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) plats = append(plats, p)
} }
return plats, nil return plats, nil

View File

@ -102,10 +102,11 @@ func (s *composeService) getDrivers(ctx context.Context) ([]build.DriverInfo, er
continue continue
} }
} }
f = driver.GetFactory(ng.Driver, true)
if f == nil { if f == nil {
if f = driver.GetFactory(ng.Driver, true); f == nil {
return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver) return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver)
} }
}
} else { } else {
ep := ng.Nodes[0].Endpoint ep := ng.Nodes[0].Endpoint
dockerapi, err := clientForEndpoint(s.dockerCli, ep) dockerapi, err := clientForEndpoint(s.dockerCli, ep)

View File

@ -248,19 +248,12 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
c := NewParallelCLI(t) c := NewParallelCLI(t)
// declare builder // declare builder
result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap", "--driver-opt", result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap")
"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")
assert.NilError(t, result.Error) assert.NilError(t, result.Error)
t.Cleanup(func() { t.Cleanup(func() {
c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down") c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down")
_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform") _ = 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) { 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) { t.Run("multi-arch build ok", func(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build") res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
assert.NilError(t, res.Error, res.Stderr()) 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: "I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`})
}) })
@ -285,16 +277,12 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
"-f", "fixtures/build-test/platforms/compose-multiple-platform-builds.yaml", "build") "-f", "fixtures/build-test/platforms/compose-multiple-platform-builds.yaml", "build")
assert.NilError(t, res.Error, res.Stderr()) 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: "I'm Service A and I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/amd64"})
res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/arm64"})
res = c.RunDockerCmd(t, "manifest", "inspect", "--insecure", "localhost:5001/build-test-platform-b:test") res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/amd64"})
res.Assert(t, icmd.Expected{Out: `"architecture": "amd64",`}) res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/arm64"})
res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/amd64"})
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",`})
}) })
t.Run("multi-arch up --build", func(t *testing.T) { 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()) assert.NilError(t, res.Error, res.Stderr())
res.Assert(t, icmd.Expected{Out: "platforms-platforms-1 exited with code 0"}) 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) { 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"`, 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"`,
})
})
} }

View File

@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM 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 FROM alpine
COPY --from=build /log /log COPY --from=build /log /log

View File

@ -1,20 +1,20 @@
services: services:
serviceA: serviceA:
image: localhost:5001/build-test-platform-a:test image: build-test-platform-a:test
build: build:
context: ./contextServiceA context: ./contextServiceA
platforms: platforms:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
serviceB: serviceB:
image: localhost:5001/build-test-platform-b:test image: build-test-platform-b:test
build: build:
context: ./contextServiceB context: ./contextServiceB
platforms: platforms:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
serviceC: serviceC:
image: localhost:5001/build-test-platform-c:test image: build-test-platform-c:test
build: build:
context: ./contextServiceC context: ./contextServiceC
platforms: platforms:

View File

@ -1,6 +1,6 @@
services: services:
platforms: platforms:
image: localhost:5001/build-test-platform:test image: build-test-platform:test
build: build:
context: . context: .
platforms: platforms:

View File

@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM 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 FROM alpine
COPY --from=build /log /log COPY --from=build /log /log

View File

@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM 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 FROM alpine
COPY --from=build /log /log COPY --from=build /log /log

View File

@ -16,7 +16,7 @@ FROM --platform=$BUILDPLATFORM golang:alpine AS build
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG BUILDPLATFORM 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 FROM alpine
COPY --from=build /log /log COPY --from=build /log /log

30
pkg/utils/slices.go Normal file
View File

@ -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
}

95
pkg/utils/slices_test.go Normal file
View File

@ -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)
}
})
}
}