瀏覽代碼

Merge pull request #9729 from glours/add-platforms-build

Add platforms build
Guillaume Lours 3 年之前
父節點
當前提交
0ac0e29294

+ 35 - 0
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:blank-imports
+	_ "github.com/moby/buildkit/util/tracing/env"              //nolint:blank-imports
+)
+
+func init() {
+	detect.ServiceName = "compose"
+	// do not log tracing errors to stdio
+	otel.SetErrorHandler(skipErrors{})
+}
+
+type skipErrors struct{}
+
+func (skipErrors) Handle(err error) {}

+ 10 - 2
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 (

+ 9 - 0
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=

+ 56 - 15
pkg/compose/build.go

@@ -81,6 +81,12 @@ 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{},
+			}}
+		}
 		opts[imageName] = buildOptions
 	}
 
@@ -162,6 +168,15 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
 			if err != nil {
 				return nil, err
 			}
+			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
 		}
@@ -206,7 +221,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 +230,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 +356,40 @@ 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) {
+	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)
+	}
+
+	for _, buildPlatform := range service.Build.Platforms {
+		p, err := platforms.Parse(buildPlatform)
+		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
+}

+ 204 - 12
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: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"
 )
 
-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
@@ -47,8 +56,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
 	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, driverInfo, opts, nil, 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
@@ -71,3 +79,187 @@ 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
+			}
+		}
+		if f == nil {
+			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
+		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)
+}
+
+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)
+}

+ 4 - 0
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)

+ 102 - 0
pkg/e2e/build_test.go

@@ -243,3 +243,105 @@ 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")
+	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")
+	})
+
+	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.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.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) {
+	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"`,
+		})
+	})
+
+	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"`,
+		})
+	})
+}

+ 22 - 0
pkg/e2e/fixtures/build-test/platforms/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 am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 23 - 0
pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml

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

+ 9 - 0
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

+ 8 - 0
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

+ 9 - 0
pkg/e2e/fixtures/build-test/platforms/compose.yaml

@@ -0,0 +1,9 @@
+services:
+  platforms:
+    image: build-test-platform:test
+    build:
+      context: .
+      platforms:
+        - linux/amd64
+        - linux/arm64
+

+ 22 - 0
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 building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 22 - 0
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 building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 22 - 0
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 building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 30 - 0
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
+}

+ 95 - 0
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)
+			}
+		})
+	}
+}