mirror of
https://github.com/docker/compose.git
synced 2025-06-22 14:40:13 +02:00
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:
parent
8ed2d8ad07
commit
e016faac33
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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"`,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
30
pkg/utils/slices.go
Normal 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
95
pkg/utils/slices_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user