From 8b1b70833ee52d3880b044984092e2dbffa72dd6 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 8 Aug 2022 16:03:36 +0200 Subject: [PATCH 1/5] add support of platforms in build section Signed-off-by: Guillaume Lours --- cmd/compose/tracing.go | 35 +++ go.mod | 12 +- go.sum | 9 + pkg/compose/build.go | 55 +++-- pkg/compose/build_buildkit.go | 203 +++++++++++++++++- pkg/compose/build_classic.go | 4 + pkg/e2e/build_test.go | 70 ++++++ .../fixtures/build-test/platforms/Dockerfile | 17 ++ ...rvice-platform-not-in-build-platforms.yaml | 9 + .../compose-unsupported-platform.yml | 8 + .../build-test/platforms/compose.yaml | 10 + 11 files changed, 404 insertions(+), 28 deletions(-) create mode 100644 cmd/compose/tracing.go create mode 100644 pkg/e2e/fixtures/build-test/platforms/Dockerfile create mode 100644 pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml create mode 100644 pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml create mode 100644 pkg/e2e/fixtures/build-test/platforms/compose.yaml diff --git a/cmd/compose/tracing.go b/cmd/compose/tracing.go new file mode 100644 index 00000000..f8ae7c29 --- /dev/null +++ b/cmd/compose/tracing.go @@ -0,0 +1,35 @@ +/* + 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 compose + +import ( + "github.com/moby/buildkit/util/tracing/detect" + "go.opentelemetry.io/otel" + + _ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:revive + _ "github.com/moby/buildkit/util/tracing/env" //nolint:revive +) + +func init() { + detect.ServiceName = "compose" + // do not log tracing errors to stdio + otel.SetErrorHandler(skipErrors{}) +} + +type skipErrors struct{} + +func (skipErrors) Handle(err error) {} diff --git a/go.mod b/go.mod index 1711a404..1e767eb3 100644 --- a/go.mod +++ b/go.mod @@ -101,7 +101,7 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.29.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.29.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect - go.opentelemetry.io/otel v1.4.1 // indirect + go.opentelemetry.io/otel v1.4.1 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 // indirect go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect go.opentelemetry.io/otel/metric v0.27.0 // indirect @@ -122,7 +122,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apimachinery v0.24.1 // indirect; see replace for the actual version used - k8s.io/client-go v0.24.1 // indirect; see replace for the actual version used + k8s.io/client-go v0.24.1 // see replace for the actual version used k8s.io/klog/v2 v2.60.1 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect @@ -130,9 +130,17 @@ require ( ) require ( + github.com/cenkalti/backoff/v4 v4.1.2 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4 // indirect + github.com/googleapis/gnostic v0.5.5 // indirect + github.com/moby/spdystream v0.2.0 // indirect + github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 // indirect github.com/zmap/zcrypto v0.0.0-20220605182715-4dfcec6e9a8c // indirect github.com/zmap/zlint v1.1.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1 // indirect + k8s.io/api v0.24.1 // indirect ) replace ( diff --git a/go.sum b/go.sum index 640374cd..c052fe4d 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,7 @@ github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMS github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -498,6 +499,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8= github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -761,6 +763,7 @@ github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsC github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gookit/color v1.2.4/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= @@ -1022,6 +1025,7 @@ github.com/moby/buildkit v0.10.4 h1:FvC+buO8isGpUFZ1abdSLdGHZVqg9sqI4BbFL8tlzP4= github.com/moby/buildkit v0.10.4/go.mod h1:Yajz9vt1Zw5q9Pp4pdb3TCSUXJBIroIQGQ3TTs/sLug= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/mount v0.1.0/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74= github.com/moby/sys/mount v0.1.1/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74= @@ -1235,6 +1239,7 @@ github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+y github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 h1:ka9QPuQg2u4LGipiZGsgkg3rJCo4iIUCy75FddM0GRQ= github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc= github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= @@ -1465,13 +1470,16 @@ go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdT go.opentelemetry.io/otel/exporters/jaeger v1.4.1/go.mod h1:ZW7vkOu9nC1CxsD8bHNHCia5JUbwP39vxgd1q4Z5rCI= go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1 h1:imIM3vRDMyZK1ypQlQlO+brE22I9lRhJsBDXpDWjlz8= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.1/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1 h1:WPpPsAAs8I2rA47v5u0558meKmmwm1Dj99ZbqCV8sZ8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.1/go.mod h1:o5RW5o2pKpJLD5dNTCmjF1DorYwMeFJmb/rKr5sLaa8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1 h1:AxqDiGk8CorEXStMDZF5Hz9vo9Z7ZZ+I5m8JRl/ko40= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.1/go.mod h1:c6E4V3/U+miqjs/8l950wggHGL1qzlp0Ypj9xoGrPqo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1 h1:8qOago/OqoFclMUUj/184tZyRdDZFpcejSjbk5Jrl6Y= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.4.1/go.mod h1:VwYo0Hak6Efuy0TXsZs8o1hnV3dHDPNtDbycG0hI8+M= go.opentelemetry.io/otel/internal/metric v0.27.0 h1:9dAVGAfFiiEq5NVB9FUJ5et+btbDQAUIJehJ+ikyryk= go.opentelemetry.io/otel/internal/metric v0.27.0/go.mod h1:n1CVxRqKqYZtqyTh9U/onvKapPGv7y/rpyOTI+LFNzw= @@ -1498,6 +1506,7 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 49773773..db8df618 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -81,6 +81,14 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti Attrs: map[string]string{"ref": image}, }) } + if len(buildOptions.Platforms) > 1 { + buildOptions.Exports = []bclient.ExportEntry{{ + Type: "image", + Attrs: map[string]string{ + "push": "true", + }, + }} + } opts[imageName] = buildOptions } @@ -162,6 +170,11 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri if err != nil { return nil, err } + if len(opt.Platforms) > 1 { + opt.Exports = []bclient.ExportEntry{{ + Type: "docker", + }} + } opts[imageName] = opt continue } @@ -206,7 +219,7 @@ func (s *composeService) doBuild(ctx context.Context, project *types.Project, op if buildkitEnabled, err := s.dockerCli.BuildKitEnabled(); err != nil || !buildkitEnabled { return s.doBuildClassic(ctx, project, opts) } - return s.doBuildBuildkit(ctx, project, opts, mode) + return s.doBuildBuildkit(ctx, opts, mode) } func (s *composeService) toBuildOptions(project *types.Project, service types.ServiceConfig, imageTag string, sshKeys []types.SSHKey) (build.Options, error) { @@ -215,20 +228,9 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se buildArgs := flatten(service.Build.Args.Resolve(envResolver(project.Environment))) - var plats []specs.Platform - if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok { - p, err := platforms.Parse(platform) - if err != nil { - return build.Options{}, err - } - plats = append(plats, p) - } - if service.Platform != "" { - p, err := platforms.Parse(service.Platform) - if err != nil { - return build.Options{}, err - } - plats = append(plats, p) + plats, err := addPlatforms(project, service) + if err != nil { + return build.Options{}, err } cacheFrom, err := buildflags.ParseCacheEntry(service.Build.CacheFrom) @@ -352,3 +354,26 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess } return secretsprovider.NewSecretProvider(store), nil } + +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) + } + 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) + } + + for _, buildPlatform := range service.Build.Platforms { + p, err := platforms.Parse(buildPlatform) + 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 d4120ced..4e8812f5 100644 --- a/pkg/compose/build_buildkit.go +++ b/pkg/compose/build_buildkit.go @@ -18,27 +18,36 @@ package compose import ( "context" + "fmt" + "net/url" "os" "path/filepath" + "strings" + + ctxkube "github.com/docker/buildx/driver/kubernetes/context" + "github.com/docker/buildx/store" + "github.com/docker/buildx/store/storeutil" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/context/docker" + ctxstore "github.com/docker/cli/cli/context/store" + dockerclient "github.com/docker/docker/client" + "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" + "k8s.io/client-go/tools/clientcmd" - "github.com/compose-spec/compose-go/types" "github.com/docker/buildx/build" "github.com/docker/buildx/driver" + _ "github.com/docker/buildx/driver/docker" //nolint:revive + _ "github.com/docker/buildx/driver/docker-container" //nolint:revive + _ "github.com/docker/buildx/driver/kubernetes" //nolint:revive xprogress "github.com/docker/buildx/util/progress" ) -func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) { - const drivername = "default" - d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient(), s.configFile(), nil, nil, nil, nil, nil, project.WorkingDir) +func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]build.Options, mode string) (map[string]string, error) { + dis, err := s.getDrivers(ctx) if err != nil { return nil, err } - driverInfo := []build.DriverInfo{ - { - Name: drivername, - Driver: d, - }, - } // Progress needs its own context that lives longer than the // build one otherwise it won't read all the messages from @@ -48,7 +57,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro w := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode) // We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here - response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile().Filename), w) + response, err := build.Build(ctx, dis, opts, nil, filepath.Dir(s.configFile().Filename), w) errW := w.Wait() if err == nil { err = errW @@ -71,3 +80,175 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro return imagesBuilt, err } + +func (s *composeService) getDrivers(ctx context.Context) ([]build.DriverInfo, error) { //nolint:gocyclo + txn, release, err := storeutil.GetStore(s.dockerCli) + if err != nil { + return nil, err + } + defer release() + + ng, err := storeutil.GetCurrentInstance(txn, s.dockerCli) + if err != nil { + return nil, err + } + + dis := make([]build.DriverInfo, len(ng.Nodes)) + var f driver.Factory + if ng.Driver != "" { + factories := driver.GetFactories() + for _, fac := range factories { + if fac.Name() == ng.Driver { + f = fac + continue + } + } + f = driver.GetFactory(ng.Driver, true) + if f == nil { + return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver) + } + } else { + ep := ng.Nodes[0].Endpoint + dockerapi, err := clientForEndpoint(s.dockerCli, ep) + if err != nil { + return nil, err + } + f, err = driver.GetDefaultFactory(ctx, dockerapi, false) + if err != nil { + return nil, err + } + ng.Driver = f.Name() + } + + imageopt, err := storeutil.GetImageConfig(s.dockerCli, ng) + if err != nil { + return nil, err + } + + eg, _ := errgroup.WithContext(ctx) + for i, n := range ng.Nodes { + func(i int, n store.Node) { + eg.Go(func() error { + di := build.DriverInfo{ + Name: n.Name, + Platform: n.Platforms, + ProxyConfig: storeutil.GetProxyConfig(s.dockerCli), + } + defer func() { + dis[i] = di + }() + + dockerapi, err := clientForEndpoint(s.dockerCli, n.Endpoint) + if err != nil { + di.Err = err + return nil + } + // TODO: replace the following line with dockerclient.WithAPIVersionNegotiation option in clientForEndpoint + dockerapi.NegotiateAPIVersion(ctx) + + contextStore := s.dockerCli.ContextStore() + + var kcc driver.KubeClientConfig + kcc, err = configFromContext(n.Endpoint, contextStore) + if err != nil { + // err is returned if n.Endpoint is non-context name like "unix:///var/run/docker.sock". + // try again with name="default". + // FIXME: n should retain real context name. + kcc, err = configFromContext("default", contextStore) + if err != nil { + logrus.Error(err) + } + } + + tryToUseKubeConfigInCluster := false + if kcc == nil { + tryToUseKubeConfigInCluster = true + } else { + if _, err := kcc.ClientConfig(); err != nil { + tryToUseKubeConfigInCluster = true + } + } + if tryToUseKubeConfigInCluster { + kccInCluster := driver.KubeClientConfigInCluster{} + if _, err := kccInCluster.ClientConfig(); err == nil { + logrus.Debug("using kube config in cluster") + kcc = kccInCluster + } + } + + d, err := driver.GetDriver(ctx, "buildx_buildkit_"+n.Name, f, dockerapi, imageopt.Auth, kcc, n.Flags, n.Files, n.DriverOpts, n.Platforms, "") + if err != nil { + di.Err = err + return nil + } + di.Driver = d + di.ImageOpt = imageopt + return nil + }) + }(i, n) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + return dis, nil +} + +func clientForEndpoint(dockerCli command.Cli, name string) (dockerclient.APIClient, error) { + list, err := dockerCli.ContextStore().List() + if err != nil { + return nil, err + } + for _, l := range list { + if l.Name != name { + continue + } + dep, ok := l.Endpoints["docker"] + if !ok { + return nil, fmt.Errorf("context %q does not have a Docker endpoint", name) + } + epm, ok := dep.(docker.EndpointMeta) + if !ok { + return nil, fmt.Errorf("endpoint %q is not of type EndpointMeta, %T", dep, dep) + } + ep, err := docker.WithTLSData(dockerCli.ContextStore(), name, epm) + if err != nil { + return nil, err + } + clientOpts, err := ep.ClientOpts() + if err != nil { + return nil, err + } + return dockerclient.NewClientWithOpts(clientOpts...) + } + + ep := docker.Endpoint{ + EndpointMeta: docker.EndpointMeta{ + Host: name, + }, + } + + clientOpts, err := ep.ClientOpts() + if err != nil { + return nil, err + } + + return dockerclient.NewClientWithOpts(clientOpts...) +} + +func configFromContext(endpointName string, s ctxstore.Reader) (clientcmd.ClientConfig, error) { + if strings.HasPrefix(endpointName, "kubernetes://") { + u, _ := url.Parse(endpointName) + if kubeconfig := u.Query().Get("kubeconfig"); kubeconfig != "" { + _ = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, kubeconfig) + } + rules := clientcmd.NewDefaultClientConfigLoadingRules() + apiConfig, err := rules.Load() + if err != nil { + return nil, err + } + return clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}), nil + } + return ctxkube.ConfigFromContext(endpointName, s) +} diff --git a/pkg/compose/build_classic.go b/pkg/compose/build_classic.go index e362ae24..3a41e618 100644 --- a/pkg/compose/build_classic.go +++ b/pkg/compose/build_classic.go @@ -89,6 +89,10 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options } } + if len(options.Platforms) > 1 { + return "", errors.Errorf("this builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use multi-arch builder") + } + switch { case isLocalDir(specifiedContext): contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, dockerfileName) diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 178ed7f9..30fb9a4a 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -243,3 +243,73 @@ func TestBuildImageDependencies(t *testing.T) { t.Skip("See https://github.com/docker/compose/issues/9232") }) } + +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") + assert.NilError(t, result.Error) + + t.Cleanup(func() { + _ = 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) { + res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", + "-f", "fixtures/build-test/platforms/compose-unsupported-platform.yml", "build") + res.Assert(t, icmd.Expected{ + ExitCode: 17, + Err: "failed to solve: alpine: no match for platform in", + }) + }) + + 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",`}) + + }) +} + +func TestBuildPlatformsStandardErrors(t *testing.T) { + c := NewParallelCLI(t) + + t.Run("no platform support with Classic Builder", 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_BUILDKIT=0") + }) + res.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: "this builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use multi-arch builder", + }) + }) + + t.Run("builder does not support multi-arch", func(t *testing.T) { + res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build") + res.Assert(t, icmd.Expected{ + ExitCode: 17, + Err: `multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`, + }) + }) + + t.Run("service platform not defined in platforms build section", func(t *testing.T) { + res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", + "-f", "fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml", "build") + res.Assert(t, icmd.Expected{ + ExitCode: 1, + Err: `service.platform should be part of the service.build.platforms: "linux/riscv64"`, + }) + }) +} diff --git a/pkg/e2e/fixtures/build-test/platforms/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/Dockerfile new file mode 100644 index 00000000..8f59df16 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/Dockerfile @@ -0,0 +1,17 @@ +# 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. + +FROM alpine + +RUN echo "SUCCESS" diff --git a/pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml b/pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml new file mode 100644 index 00000000..bed88fa5 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml @@ -0,0 +1,9 @@ +services: + platforms: + image: build-test-platform:test + platform: linux/riscv64 + build: + context: . + platforms: + - linux/amd64 + - linux/arm64 diff --git a/pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml b/pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml new file mode 100644 index 00000000..e3342829 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml @@ -0,0 +1,8 @@ +services: + platforms: + image: build-test-platform:test + build: + context: . + platforms: + - unsupported/unsupported + - linux/amd64 diff --git a/pkg/e2e/fixtures/build-test/platforms/compose.yaml b/pkg/e2e/fixtures/build-test/platforms/compose.yaml new file mode 100644 index 00000000..2e16fbe3 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/compose.yaml @@ -0,0 +1,10 @@ +services: + platforms: + image: localhost:5001/build-test-platform:test + platform: linux/amd64 + build: + context: . + platforms: + - linux/amd64 + - linux/arm64 + From 537f023a3b024d38531a9313848dcdd968eb08cd Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:36:32 +0200 Subject: [PATCH 2/5] fix panic when using 'compose up --build' Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- cmd/compose/tracing.go | 4 +- pkg/compose/build.go | 4 ++ pkg/compose/build_buildkit.go | 38 ++++++++++++++++--- pkg/e2e/build_test.go | 7 ++++ .../fixtures/build-test/platforms/Dockerfile | 9 ++++- .../build-test/platforms/compose.yaml | 1 - 6 files changed, 53 insertions(+), 10 deletions(-) diff --git a/cmd/compose/tracing.go b/cmd/compose/tracing.go index f8ae7c29..99ff58d8 100644 --- a/cmd/compose/tracing.go +++ b/cmd/compose/tracing.go @@ -20,8 +20,8 @@ import ( "github.com/moby/buildkit/util/tracing/detect" "go.opentelemetry.io/otel" - _ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:revive - _ "github.com/moby/buildkit/util/tracing/env" //nolint:revive + _ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports + _ "github.com/moby/buildkit/util/tracing/env" //nolint:blank-imports ) func init() { diff --git a/pkg/compose/build.go b/pkg/compose/build.go index db8df618..6282e7a8 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -173,7 +173,11 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri if len(opt.Platforms) > 1 { opt.Exports = []bclient.ExportEntry{{ Type: "docker", + Attrs: map[string]string{ + "load": "true", + }, }} + opt.Platforms = []specs.Platform{} } opts[imageName] = opt continue diff --git a/pkg/compose/build_buildkit.go b/pkg/compose/build_buildkit.go index 4e8812f5..fb3981ab 100644 --- a/pkg/compose/build_buildkit.go +++ b/pkg/compose/build_buildkit.go @@ -37,9 +37,9 @@ import ( "github.com/docker/buildx/build" "github.com/docker/buildx/driver" - _ "github.com/docker/buildx/driver/docker" //nolint:revive - _ "github.com/docker/buildx/driver/docker-container" //nolint:revive - _ "github.com/docker/buildx/driver/kubernetes" //nolint:revive + _ "github.com/docker/buildx/driver/docker" //nolint:blank-imports + _ "github.com/docker/buildx/driver/docker-container" //nolint:blank-imports + _ "github.com/docker/buildx/driver/kubernetes" //nolint:blank-imports xprogress "github.com/docker/buildx/util/progress" ) @@ -56,8 +56,10 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]bu defer cancel() w := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode) - // We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here - response, err := build.Build(ctx, dis, opts, nil, filepath.Dir(s.configFile().Filename), w) + // Get the DockerAPI if a "docker" export is defined (ie: up and run command), otherwise get nil and let use the default buildx builder + API := getDockerAPI(s.dockerCli, opts) + + response, err := build.Build(ctx, dis, opts, API, filepath.Dir(s.configFile().Filename), w) errW := w.Wait() if err == nil { err = errW @@ -252,3 +254,29 @@ func configFromContext(endpointName string, s ctxstore.Reader) (clientcmd.Client } return ctxkube.ConfigFromContext(endpointName, s) } + +type internalAPI struct { + dockerCli command.Cli +} + +func (a *internalAPI) DockerAPI(name string) (dockerclient.APIClient, error) { + if name == "" { + name = a.dockerCli.CurrentContext() + } + return clientForEndpoint(a.dockerCli, name) +} + +func dockerAPI(dockerCli command.Cli) *internalAPI { + return &internalAPI{dockerCli: dockerCli} +} + +func getDockerAPI(cli command.Cli, opts map[string]build.Options) *internalAPI { + for _, opt := range opts { + for _, export := range opt.Exports { + if export.Type == "docker" { + return dockerAPI(cli) + } + } + } + return nil +} diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 30fb9a4a..7aacf2b5 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -258,6 +258,7 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { 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") }) @@ -279,6 +280,12 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { res.Assert(t, icmd.Expected{Out: `"architecture": "arm64",`}) }) + + 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"}) + }) } func TestBuildPlatformsStandardErrors(t *testing.T) { diff --git a/pkg/e2e/fixtures/build-test/platforms/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/Dockerfile index 8f59df16..643926d8 100644 --- a/pkg/e2e/fixtures/build-test/platforms/Dockerfile +++ b/pkg/e2e/fixtures/build-test/platforms/Dockerfile @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM alpine +FROM --platform=$BUILDPLATFORM golang:alpine AS build -RUN echo "SUCCESS" +ARG TARGETPLATFORM +ARG BUILDPLATFORM +RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log + +FROM alpine +COPY --from=build /log /log diff --git a/pkg/e2e/fixtures/build-test/platforms/compose.yaml b/pkg/e2e/fixtures/build-test/platforms/compose.yaml index 2e16fbe3..e8ba350a 100644 --- a/pkg/e2e/fixtures/build-test/platforms/compose.yaml +++ b/pkg/e2e/fixtures/build-test/platforms/compose.yaml @@ -1,7 +1,6 @@ services: platforms: image: localhost:5001/build-test-platform:test - platform: linux/amd64 build: context: . platforms: From 8ed2d8ad07115fa5dd017e0ac530c3fcf85ef745 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:13:03 +0200 Subject: [PATCH 3/5] add a test with multiple service builds using platforms in the same compose file Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- pkg/compose/build_buildkit.go | 20 +--------------- pkg/e2e/build_test.go | 16 +++++++++++++ .../compose-multiple-platform-builds.yaml | 23 +++++++++++++++++++ .../platforms/contextServiceA/Dockerfile | 22 ++++++++++++++++++ .../platforms/contextServiceB/Dockerfile | 22 ++++++++++++++++++ .../platforms/contextServiceC/Dockerfile | 22 ++++++++++++++++++ 6 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml create mode 100644 pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile create mode 100644 pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile create mode 100644 pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile diff --git a/pkg/compose/build_buildkit.go b/pkg/compose/build_buildkit.go index fb3981ab..b11251ec 100644 --- a/pkg/compose/build_buildkit.go +++ b/pkg/compose/build_buildkit.go @@ -56,10 +56,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, opts map[string]bu defer cancel() w := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode) - // Get the DockerAPI if a "docker" export is defined (ie: up and run command), otherwise get nil and let use the default buildx builder - API := getDockerAPI(s.dockerCli, opts) - - response, err := build.Build(ctx, dis, opts, API, filepath.Dir(s.configFile().Filename), w) + response, err := build.Build(ctx, dis, opts, &internalAPI{dockerCli: s.dockerCli}, filepath.Dir(s.configFile().Filename), w) errW := w.Wait() if err == nil { err = errW @@ -265,18 +262,3 @@ func (a *internalAPI) DockerAPI(name string) (dockerclient.APIClient, error) { } return clientForEndpoint(a.dockerCli, name) } - -func dockerAPI(dockerCli command.Cli) *internalAPI { - return &internalAPI{dockerCli: dockerCli} -} - -func getDockerAPI(cli command.Cli, opts map[string]build.Options) *internalAPI { - for _, opt := range opts { - for _, export := range opt.Exports { - if export.Type == "docker" { - return dockerAPI(cli) - } - } - } - return nil -} diff --git a/pkg/e2e/build_test.go b/pkg/e2e/build_test.go index 7aacf2b5..b91d42b6 100644 --- a/pkg/e2e/build_test.go +++ b/pkg/e2e/build_test.go @@ -281,6 +281,22 @@ func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) { }) + 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",`}) + + }) + 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()) 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 new file mode 100644 index 00000000..0f8ce993 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml @@ -0,0 +1,23 @@ +services: + serviceA: + image: localhost:5001/build-test-platform-a:test + build: + context: ./contextServiceA + platforms: + - linux/amd64 + - linux/arm64 + serviceB: + image: localhost:5001/build-test-platform-b:test + build: + context: ./contextServiceB + platforms: + - linux/amd64 + - linux/arm64 + serviceC: + image: localhost:5001/build-test-platform-c:test + build: + context: ./contextServiceC + platforms: + - linux/amd64 + - linux/arm64 + diff --git a/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile b/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile new file mode 100644 index 00000000..057ed864 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile @@ -0,0 +1,22 @@ +# 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. + +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 + +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 new file mode 100644 index 00000000..88eecb90 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile @@ -0,0 +1,22 @@ +# 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. + +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 + +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 new file mode 100644 index 00000000..1b917299 --- /dev/null +++ b/pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile @@ -0,0 +1,22 @@ +# 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. + +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 + +FROM alpine +COPY --from=build /log /log From e016faac33c7d0ad8b58e63b47fa5eab3dc73039 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:53:41 +0200 Subject: [PATCH 4/5] 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> --- pkg/compose/build.go | 38 +++++--- pkg/compose/build_buildkit.go | 5 +- pkg/e2e/build_test.go | 51 ++++++---- .../fixtures/build-test/platforms/Dockerfile | 2 +- .../compose-multiple-platform-builds.yaml | 6 +- .../build-test/platforms/compose.yaml | 2 +- .../platforms/contextServiceA/Dockerfile | 2 +- .../platforms/contextServiceB/Dockerfile | 2 +- .../platforms/contextServiceC/Dockerfile | 2 +- pkg/utils/slices.go | 30 ++++++ pkg/utils/slices_test.go | 95 +++++++++++++++++++ 11 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 pkg/utils/slices.go create mode 100644 pkg/utils/slices_test.go diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 6282e7a8..2583039c 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 b11251ec..912c520c 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 b91d42b6..7eeebf94 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 643926d8..ef22c17f 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 0f8ce993..aac3a3db 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 e8ba350a..73421f47 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 057ed864..468b2b10 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 88eecb90..cfa2ae34 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 1b917299..3216f618 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 00000000..3b635c25 --- /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 00000000..d9468afe --- /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) + } + }) + } +} From 44c55e89c072b603933db296b8ee5f87ac343918 Mon Sep 17 00:00:00 2001 From: Guillaume Lours <705411+glours@users.noreply.github.com> Date: Fri, 2 Sep 2022 15:36:28 +0200 Subject: [PATCH 5/5] always use 'docker' export entry when building with 'up' or 'run' commands Signed-off-by: Guillaume Lours <705411+glours@users.noreply.github.com> --- pkg/compose/build.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pkg/compose/build.go b/pkg/compose/build.go index 2583039c..6079d853 100644 --- a/pkg/compose/build.go +++ b/pkg/compose/build.go @@ -168,16 +168,14 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri if err != nil { return nil, err } - if len(opt.Platforms) > 1 { - opt.Exports = []bclient.ExportEntry{{ - Type: "docker", - Attrs: map[string]string{ - "load": "true", - }, - }} - if opt.Platforms, err = useDockerDefaultPlatform(project, service.Build.Platforms); err != nil { - opt.Platforms = []specs.Platform{} - } + opt.Exports = []bclient.ExportEntry{{ + Type: "docker", + Attrs: map[string]string{ + "load": "true", + }, + }} + if opt.Platforms, err = useDockerDefaultPlatform(project, service.Build.Platforms); err != nil { + opt.Platforms = []specs.Platform{} } opts[imageName] = opt continue