Skip to content

Commit

Permalink
don't push images at the end of multi-arch build (and simplify e2e te…
Browse files Browse the repository at this point in the history
…sts)

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>
  • Loading branch information
glours committed Aug 31, 2022
1 parent 03810d4 commit 0ac4fa2
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 43 deletions.
38 changes: 26 additions & 12 deletions pkg/compose/build.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions pkg/compose/build_buildkit.go
Expand Up @@ -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
Expand Down
51 changes: 30 additions & 21 deletions pkg/e2e/build_test.go
Expand Up @@ -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) {
Expand All @@ -275,33 +268,38 @@ 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"})

})

t.Run("multi-arch multi service builds ok", func(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) {
res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build")
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) {
Expand Down Expand Up @@ -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"`,
})
})
}
2 changes: 1 addition & 1 deletion pkg/e2e/fixtures/build-test/platforms/Dockerfile
Expand Up @@ -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
@@ -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:
Expand Down
2 changes: 1 addition & 1 deletion 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:
Expand Down
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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
14 changes: 14 additions & 0 deletions pkg/utils/slices.go
@@ -0,0 +1,14 @@
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
}
78 changes: 78 additions & 0 deletions pkg/utils/slices_test.go
@@ -0,0 +1,78 @@
package utils

import (
specs "github.com/opencontainers/image-spec/specs-go/v1"
"testing"
)

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

0 comments on commit 0ac4fa2

Please sign in to comment.