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

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

View File

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

View File

@ -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"`,
})
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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