From 006502d05b85a5f1cb62f4158107acedfae3ba95 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 8 Aug 2022 16:03:36 +0200 Subject: [PATCH] add support of platforms in build section Signed-off-by: Guillaume Lours --- cmd/compose/tracing.go | 19 ++ go.mod | 12 +- go.sum | 9 + pkg/compose/build.go | 49 +++-- pkg/compose/build_buildkit.go | 198 +++++++++++++++++- 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 + pkg/e2e/framework.go | 17 ++ 12 files changed, 397 insertions(+), 25 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 00000000000..a3c69ceb947 --- /dev/null +++ b/cmd/compose/tracing.go @@ -0,0 +1,19 @@ +package compose + +import ( + "github.com/moby/buildkit/util/tracing/detect" + "go.opentelemetry.io/otel" + + _ "github.com/moby/buildkit/util/tracing/detect/delegated" + _ "github.com/moby/buildkit/util/tracing/env" +) + +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 0e1c7408b26..d938c1c510c 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 af6f138364a..6b8a72507ac 100644 --- a/go.sum +++ b/go.sum @@ -250,6 +250,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= @@ -500,6 +501,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= @@ -766,6 +768,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= @@ -1027,6 +1030,7 @@ github.com/moby/buildkit v0.10.3 h1:/dGykD8FW+H4p++q5+KqKEo6gAkYKyBQHdawdjVwVAU= github.com/moby/buildkit v0.10.3/go.mod h1:jxeOuly98l9gWHai0Ojrbnczrk/rf+o9/JqNhY+UCSo= 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= @@ -1241,6 +1245,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= @@ -1471,13 +1476,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= @@ -1504,6 +1512,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 995ada52f70..d806b655d13 100644 --- a/pkg/compose/build.go +++ b/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}, }) } + buildOptions.Exports = []bclient.ExportEntry{{ + Type: "image", + Attrs: map[string]string{ + "push": "true", + }, + }} opts[imageName] = buildOptions } @@ -161,6 +167,9 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri if err != nil { return nil, err } + opt.Exports = []bclient.ExportEntry{{ + Type: "docker", + }} opts[imageName] = opt continue } @@ -213,20 +222,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) @@ -350,3 +348,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 d4120ced3dd..febe6c41ba7 100644 --- a/pkg/compose/build_buildkit.go +++ b/pkg/compose/build_buildkit.go @@ -18,27 +18,36 @@ package compose import ( "context" + "fmt" + 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" + "net/url" "os" "path/filepath" + "strings" "github.com/compose-spec/compose-go/types" "github.com/docker/buildx/build" "github.com/docker/buildx/driver" + _ "github.com/docker/buildx/driver/docker" + _ "github.com/docker/buildx/driver/docker-container" + _ "github.com/docker/buildx/driver/kubernetes" 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) + 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,174 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro return imagesBuilt, err } + +func (s *composeService) getDrivers(ctx context.Context) ([]build.DriverInfo, error) { + 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 { + 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 dc88dee3ff0..cf6f8d57e4b 100644 --- a/pkg/compose/build_classic.go +++ b/pkg/compose/build_classic.go @@ -88,6 +88,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 178ed7f9b5f..30fb9a4a86f 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 00000000000..8f59df16b65 --- /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 00000000000..bed88fa51f3 --- /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 00000000000..e3342829168 --- /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 00000000000..2e16fbe3f55 --- /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 + diff --git a/pkg/e2e/framework.go b/pkg/e2e/framework.go index 66a6233a752..41019ab66d7 100644 --- a/pkg/e2e/framework.go +++ b/pkg/e2e/framework.go @@ -37,6 +37,8 @@ import ( "github.com/docker/compose/v2/cmd/compose" ) +const NoContext = "" + var ( // DockerExecutableName is the OS dependent Docker CLI binary name DockerExecutableName = "docker" @@ -281,14 +283,29 @@ func (c *CLI) RunDockerComposeCmdNoCheck(t testing.TB, args ...string) *icmd.Res return icmd.RunCmd(c.NewDockerComposeCmd(t, args...)) } +func (c *CLI) RunDockerComposeCmdContextNoCheck(t testing.TB, contextName string, args ...string) *icmd.Result { + t.Helper() + return icmd.RunCmd(c.NewDockerComposeWithContextCmd(t, contextName, args...)) +} + // NewDockerComposeCmd creates a command object for Compose, either in plugin // or standalone mode (based on build tags). func (c *CLI) NewDockerComposeCmd(t testing.TB, args ...string) icmd.Cmd { + t.Helper() + return c.NewDockerComposeWithContextCmd(t, NoContext, args...) +} + +// NewDockerComposeWithContextCmd creates a command object for Compose, either in plugin +// or standalone mode (based on build tags) using a Context. +func (c *CLI) NewDockerComposeWithContextCmd(t testing.TB, contextName string, args ...string) icmd.Cmd { t.Helper() if composeStandaloneMode { return c.NewCmd(ComposeStandalonePath(t), args...) } args = append([]string{"compose"}, args...) + if contextName != "" { + args = append([]string{"--context", contextName}, args...) + } return c.NewCmd(DockerExecutableName, args...) }