From cc4f194295f9758ce6bb51fa02215d47e49b1f9e Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 30 Aug 2022 12:42:56 +0200 Subject: [PATCH 01/18] Add E2E tests for starting/stopping single services Signed-off-by: Laura Brehm --- .../fixtures/start-stop/start-stop-deps.yaml | 17 ++++++ pkg/e2e/start_stop_test.go | 53 ++++++++++++------- 2 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 pkg/e2e/fixtures/start-stop/start-stop-deps.yaml diff --git a/pkg/e2e/fixtures/start-stop/start-stop-deps.yaml b/pkg/e2e/fixtures/start-stop/start-stop-deps.yaml new file mode 100644 index 00000000000..fb1f7fad702 --- /dev/null +++ b/pkg/e2e/fixtures/start-stop/start-stop-deps.yaml @@ -0,0 +1,17 @@ +services: + another_2: + image: nginx:alpine + another: + image: nginx:alpine + depends_on: + - another_2 + dep_2: + image: nginx:alpine + dep_1: + image: nginx:alpine + depends_on: + - dep_2 + desired: + image: nginx:alpine + depends_on: + - dep_1 diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 1a16d089d43..9640b4feffa 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -222,29 +222,46 @@ func TestStopAlreadyStopped(t *testing.T) { } func TestStartStopMultipleServices(t *testing.T) { - cli := NewParallelCLI(t, WithEnv( - "COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple", - "COMPOSE_FILE=./fixtures/start-stop/compose.yaml")) + cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple")) t.Cleanup(func() { cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0") }) - cli.RunDockerComposeCmd(t, "up", "-d", "--wait") - - res := cli.RunDockerComposeCmd(t, "stop", "simple", "another") - services := []string{"simple", "another"} - for _, svc := range services { - stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) - assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), - fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) - } + t.Run("starts/stops multiple services", func(t *testing.T) { + cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "up", "-d", "--wait") + + res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop", "simple", "another") + services := []string{"simple", "another"} + for _, svc := range services { + stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) + assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), + fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) + } + + res = cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "start", "simple", "another") + for _, svc := range services { + startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) + assert.Assert(t, strings.Contains(res.Stderr(), startMsg), + fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) + } + }) - res = cli.RunDockerComposeCmd(t, "start", "simple", "another") - for _, svc := range services { - startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) - assert.Assert(t, strings.Contains(res.Stderr(), startMsg), - fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) - } + t.Run("starts one service out of many", func(t *testing.T) { + cli.RunDockerComposeCmd(t, "down") + cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "create", "desired") + + res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "start", "desired") + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), + fmt.Sprintf("Missing start message for service: desired\n%s", res.Combined())) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_1-1 Started"), + fmt.Sprintf("Missing start message for service: dep_1\n%s", res.Combined())) + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), + fmt.Sprintf("Missing start message for service: dep_2\n%s", res.Combined())) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another-1 Started"), + fmt.Sprintf("Shouldn't have tried to start service: another\n%s", res.Combined())) + assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another_2-1 Started"), + fmt.Sprintf("Shouldn't have tried to start service: another_2\n%s", res.Combined())) + }) } func TestStartStopMultipleFiles(t *testing.T) { From 8b1b70833ee52d3880b044984092e2dbffa72dd6 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Mon, 8 Aug 2022 16:03:36 +0200 Subject: [PATCH 02/18] 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 00000000000..f8ae7c29e10 --- /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 1711a404ac2..1e767eb358b 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 640374cd2ef..c052fe4d640 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 49773773168..db8df61874f 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 d4120ced3dd..4e8812f586d 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 e362ae247d7..3a41e618b70 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 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 + 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 03/18] 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 f8ae7c29e10..99ff58d8247 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 db8df61874f..6282e7a8613 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 4e8812f586d..fb3981ab333 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 30fb9a4a86f..7aacf2b5c57 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 8f59df16b65..643926d8b8b 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 + +ARG TARGETPLATFORM +ARG BUILDPLATFORM +RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" > /log -RUN echo "SUCCESS" +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 2e16fbe3f55..e8ba350ab82 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 04/18] 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 fb3981ab333..b11251ec214 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 7aacf2b5c57..b91d42b612c 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 00000000000..0f8ce9936e0 --- /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 00000000000..057ed864cc3 --- /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 00000000000..88eecb90230 --- /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 00000000000..1b917299471 --- /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 05/18] 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 6282e7a8613..2583039c12e 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 b11251ec214..912c520c26b 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 b91d42b612c..7eeebf94840 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 643926d8b8b..ef22c17f6a5 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 0f8ce9936e0..aac3a3db90d 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 e8ba350ab82..73421f4793f 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 057ed864cc3..468b2b10dd6 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 88eecb90230..cfa2ae34ad7 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 1b917299471..3216f618295 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 00000000000..3b635c25db0 --- /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 00000000000..d9468afef29 --- /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 06/18] 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 2583039c12e..6079d853d14 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 From 361194472edb2efff9f99477721fe2ea392b5557 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 30 Aug 2022 16:37:02 +0200 Subject: [PATCH 07/18] Cleanup E2E tests Signed-off-by: Laura Brehm --- pkg/e2e/start_stop_test.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 9640b4feffa..892142c327c 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -246,21 +246,21 @@ func TestStartStopMultipleServices(t *testing.T) { } }) - t.Run("starts one service out of many", func(t *testing.T) { + t.Run("starts one service out of many, and it's dependencies", func(t *testing.T) { cli.RunDockerComposeCmd(t, "down") cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "create", "desired") res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "start", "desired") - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), - fmt.Sprintf("Missing start message for service: desired\n%s", res.Combined())) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_1-1 Started"), - fmt.Sprintf("Missing start message for service: dep_1\n%s", res.Combined())) - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), - fmt.Sprintf("Missing start message for service: dep_2\n%s", res.Combined())) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another-1 Started"), - fmt.Sprintf("Shouldn't have tried to start service: another\n%s", res.Combined())) - assert.Assert(t, !strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-another_2-1 Started"), - fmt.Sprintf("Shouldn't have tried to start service: another_2\n%s", res.Combined())) + desiredServices := []string{"desired", "dep_1", "dep_2"} + for _, s := range desiredServices { + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), + fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) + } + undesiredServices := []string{"another", "another_2"} + for _, s := range undesiredServices { + assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), + fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) + } }) } From ff53411d9d250b9a0a5c42041e6c30cd7634a437 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Sep 2022 10:09:55 +0000 Subject: [PATCH 08/18] build(deps): bump go.opentelemetry.io/otel from 1.4.1 to 1.9.0 Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.4.1 to 1.9.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.4.1...v1.9.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a61fb9269dd..668f065002d 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/fvbommel/sortorder v1.0.2 // indirect - github.com/go-logr/logr v1.2.2 // indirect + github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/flock v0.8.0 // indirect github.com/gogo/googleapis v1.4.1 // indirect @@ -101,12 +101,12 @@ 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 + go.opentelemetry.io/otel v1.9.0 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 go.opentelemetry.io/otel/sdk v1.4.1 // indirect - go.opentelemetry.io/otel/trace v1.4.1 // indirect + go.opentelemetry.io/otel/trace v1.9.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect diff --git a/go.sum b/go.sum index d6416883258..2ebd1f7b285 100644 --- a/go.sum +++ b/go.sum @@ -558,8 +558,9 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= @@ -1465,8 +1466,9 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0/go.mod h1: go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= -go.opentelemetry.io/otel v1.4.1 h1:QbINgGDDcoQUoMJa2mMaWno49lja9sHwp6aoa2n3a4g= go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= +go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw= +go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= 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= @@ -1496,8 +1498,9 @@ go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4 go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= -go.opentelemetry.io/otel/trace v1.4.1 h1:O+16qcdTrT7zxv2J6GejTPFinSwA++cYerC5iSiF8EQ= go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= +go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc= +go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= go.opentelemetry.io/proto/otlp v0.12.0 h1:CMJ/3Wp7iOWES+CYLfnBv+DVmPbB+kmy9PJ92XvlR6c= From 79af3cdd852d2f8d9257767b35fb7a6dac7ae9a2 Mon Sep 17 00:00:00 2001 From: Ulysses Souza Date: Wed, 7 Sep 2022 17:00:20 +0200 Subject: [PATCH 09/18] Apply newly loaded envvars to "DockerCli" and "APIClient" Re-evaluate DockerCli and APIClient after reading the environment file. I can contain DOCKER_HOST and/or DOCKER_CONTEXT so the DockerCli passed by docker/cli has to be re-evaluated. Also checks for DOCKER_CERT_PATH and DOCKER_TLS_VERIFY. Signed-off-by: Ulysses Souza --- cmd/compose/compose.go | 12 ++++++++++++ go.mod | 4 +++- go.sum | 8 ++++++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 350eb5e88dc..1286d68aa43 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -25,6 +25,7 @@ import ( "strings" "syscall" + cnabgodocker "github.com/cnabio/cnab-go/driver/docker" "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/types" composegoutils "github.com/compose-spec/compose-go/utils" @@ -32,6 +33,7 @@ import ( dockercli "github.com/docker/cli/cli" "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli/command" + "github.com/docker/docker/client" "github.com/morikuni/aec" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -291,6 +293,16 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { if err != nil { return err } + // Reset DockerCli and APIClient to get possible `DOCKER_HOST` and/or `DOCKER_CONTEXT` loaded from environment file. + err = dockerCli.Apply(func(cli *command.DockerCli) error { + return cli.Initialize(cnabgodocker.BuildDockerClientOptions(), + command.WithInitializeClient(func(_ *command.DockerCli) (client.APIClient, error) { + return nil, nil + })) + }) + if err != nil { + return err + } parent := cmd.Root() if parent != nil { parentPrerun := parent.PersistentPreRunE diff --git a/go.mod b/go.mod index 668f065002d..1efee8bc4e6 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/AlecAivazis/survey/v2 v2.3.5 github.com/buger/goterm v1.0.4 + github.com/cnabio/cnab-go v0.24.1-0.20220907172316-1ca5c8721bf7 github.com/cnabio/cnab-to-oci v0.3.7 github.com/compose-spec/compose-go v1.5.0 github.com/containerd/console v1.0.3 @@ -44,7 +45,6 @@ require ( github.com/Microsoft/go-winio v0.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/cnabio/cnab-go v0.23.4 // indirect github.com/containerd/continuity v0.2.3-0.20220330195504-d132b287edc8 // indirect github.com/containerd/ttrpc v1.1.0 // indirect github.com/containerd/typeurl v1.0.2 // indirect @@ -133,6 +133,8 @@ 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/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // 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 diff --git a/go.sum b/go.sum index 2ebd1f7b285..54f7502a847 100644 --- a/go.sum +++ b/go.sum @@ -269,8 +269,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v0.0.0-20181213083726-b94e044bb51e/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw= -github.com/cnabio/cnab-go v0.23.4 h1:jplQcSnvFyQlD6swiqL3BmqRnhbnS+lc/EKdBLH9E80= -github.com/cnabio/cnab-go v0.23.4/go.mod h1:9EmgHR51LFqQStzaC+xHPJlkD4OPsF6Ev5Y8e/YHEns= +github.com/cnabio/cnab-go v0.24.1-0.20220907172316-1ca5c8721bf7 h1:6cETeoyahKaH4hNShuB4KUqkTdjLVKEpTakHW5bpDW8= +github.com/cnabio/cnab-go v0.24.1-0.20220907172316-1ca5c8721bf7/go.mod h1:Zm0HTH8xxzinB64SXm7KFSna7DEN0ZjZwrRwZpfgChU= github.com/cnabio/cnab-to-oci v0.3.7 h1:wA2AG3HQMaJZhWlr3zsfVoa2m5B1R/SP+YcoFuNfP9o= github.com/cnabio/cnab-to-oci v0.3.7/go.mod h1:AvVNl0Hh3VBk1zqeLdyE5S3bTQ5EsZPPF4mUUJYyy1Y= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -1002,6 +1002,8 @@ github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= @@ -1020,6 +1022,8 @@ github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/buildkit v0.8.1/go.mod h1:/kyU1hKy/aYCuP39GZA9MaKioovHku57N6cqlKZIaiQ= github.com/moby/buildkit v0.10.1-0.20220403220257-10e6f94bf90d/go.mod h1:WvwAZv8aRScHkqc/+X46cRC2CKMKpqcaX+pRvUTtPes= github.com/moby/buildkit v0.10.4 h1:FvC+buO8isGpUFZ1abdSLdGHZVqg9sqI4BbFL8tlzP4= From 209293e449260f37e949a888149ee7f92f2a73c8 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 6 Sep 2022 20:31:42 +0200 Subject: [PATCH 10/18] Restrict compose project to selected services and dependencies on `compose start` Signed-off-by: Laura Brehm --- cmd/compose/start.go | 1 + pkg/api/api.go | 2 ++ pkg/compose/dependencies.go | 27 +++++++++++++++++---------- pkg/compose/start.go | 7 +++++++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/cmd/compose/start.go b/cmd/compose/start.go index 45da76122e4..7b7a962658d 100644 --- a/cmd/compose/start.go +++ b/cmd/compose/start.go @@ -51,5 +51,6 @@ func runStart(ctx context.Context, backend api.Service, opts startOptions, servi return backend.Start(ctx, name, api.StartOptions{ AttachTo: services, Project: project, + Services: services, }) } diff --git a/pkg/api/api.go b/pkg/api/api.go index 38af772ca7e..5e75cd57154 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -129,6 +129,8 @@ type StartOptions struct { ExitCodeFrom string // Wait won't return until containers reached the running|healthy state Wait bool + // Services passed in the command line to be started + Services []string } // RestartOptions group options of the Restart API diff --git a/pkg/compose/dependencies.go b/pkg/compose/dependencies.go index ed33ec2b2b2..75e08914fd2 100644 --- a/pkg/compose/dependencies.go +++ b/pkg/compose/dependencies.go @@ -63,21 +63,24 @@ var ( ) // InDependencyOrder applies the function to the services of the project taking in account the dependency order -func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error { - return visit(ctx, project, upDirectionTraversalConfig, fn, ServiceStopped) +func InDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error, options ...func(*graphTraversalConfig)) error { + graph, err := NewGraph(project.Services, ServiceStopped) + if err != nil { + return err + } + return visit(ctx, graph, upDirectionTraversalConfig, fn) } // InReverseDependencyOrder applies the function to the services of the project in reverse order of dependencies func InReverseDependencyOrder(ctx context.Context, project *types.Project, fn func(context.Context, string) error) error { - return visit(ctx, project, downDirectionTraversalConfig, fn, ServiceStarted) -} - -func visit(ctx context.Context, project *types.Project, traversalConfig graphTraversalConfig, fn func(context.Context, string) error, initialStatus ServiceStatus) error { - g := NewGraph(project.Services, initialStatus) - if b, err := g.HasCycles(); b { + graph, err := NewGraph(project.Services, ServiceStarted) + if err != nil { return err } + return visit(ctx, graph, downDirectionTraversalConfig, fn) +} +func visit(ctx context.Context, g *Graph, traversalConfig graphTraversalConfig, fn func(context.Context, string) error) error { nodes := traversalConfig.extremityNodesFn(g) eg, _ := errgroup.WithContext(ctx) @@ -155,7 +158,7 @@ func (v *Vertex) GetChildren() []*Vertex { } // NewGraph returns the dependency graph of the services -func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph { +func NewGraph(services types.Services, initialStatus ServiceStatus) (*Graph, error) { graph := &Graph{ lock: sync.RWMutex{}, Vertices: map[string]*Vertex{}, @@ -171,7 +174,11 @@ func NewGraph(services types.Services, initialStatus ServiceStatus) *Graph { } } - return graph + if b, err := graph.HasCycles(); b { + return nil, err + } + + return graph, nil } // NewVertex is the constructor function for the Vertex diff --git a/pkg/compose/start.go b/pkg/compose/start.go index d1af0637c95..0dcef8ded14 100644 --- a/pkg/compose/start.go +++ b/pkg/compose/start.go @@ -50,6 +50,13 @@ func (s *composeService) start(ctx context.Context, projectName string, options } } + if len(options.Services) > 0 { + err := project.ForServices(options.Services) + if err != nil { + return err + } + } + eg, ctx := errgroup.WithContext(ctx) if listener != nil { attached, err := s.attach(ctx, project, listener, options.AttachTo) From 4c474fe0291f2605d2e39da18bdc992734687e17 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Tue, 6 Sep 2022 21:12:43 +0200 Subject: [PATCH 11/18] Add unit tests to graph building logic in `dependencies.go` Signed-off-by: Laura Brehm --- pkg/compose/dependencies_test.go | 180 +++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/pkg/compose/dependencies_test.go b/pkg/compose/dependencies_test.go index 61bf0fd9c8e..baaa98ce891 100644 --- a/pkg/compose/dependencies_test.go +++ b/pkg/compose/dependencies_test.go @@ -18,10 +18,12 @@ package compose import ( "context" + "fmt" "testing" "github.com/compose-spec/compose-go/types" "github.com/stretchr/testify/require" + "gotest.tools/assert" ) var project = types.Project{ @@ -69,3 +71,181 @@ func TestInDependencyReverseDownCommandOrder(t *testing.T) { require.NoError(t, err, "Error during iteration") require.Equal(t, []string{"test1", "test2", "test3"}, order) } + +func TestBuildGraph(t *testing.T) { + testCases := []struct { + desc string + services types.Services + expectedVertices map[string]*Vertex + }{ + { + desc: "builds graph with single service", + services: types.Services{ + { + Name: "test", + DependsOn: types.DependsOnConfig{}, + }, + }, + expectedVertices: map[string]*Vertex{ + "test": { + Key: "test", + Service: "test", + Status: ServiceStopped, + Children: map[string]*Vertex{}, + Parents: map[string]*Vertex{}, + }, + }, + }, + { + desc: "builds graph with two separate services", + services: types.Services{ + { + Name: "test", + DependsOn: types.DependsOnConfig{}, + }, + { + Name: "another", + DependsOn: types.DependsOnConfig{}, + }, + }, + expectedVertices: map[string]*Vertex{ + "test": { + Key: "test", + Service: "test", + Status: ServiceStopped, + Children: map[string]*Vertex{}, + Parents: map[string]*Vertex{}, + }, + "another": { + Key: "another", + Service: "another", + Status: ServiceStopped, + Children: map[string]*Vertex{}, + Parents: map[string]*Vertex{}, + }, + }, + }, + { + desc: "builds graph with a service and a dependency", + services: types.Services{ + { + Name: "test", + DependsOn: types.DependsOnConfig{ + "another": types.ServiceDependency{}, + }, + }, + { + Name: "another", + DependsOn: types.DependsOnConfig{}, + }, + }, + expectedVertices: map[string]*Vertex{ + "test": { + Key: "test", + Service: "test", + Status: ServiceStopped, + Children: map[string]*Vertex{ + "another": {}, + }, + Parents: map[string]*Vertex{}, + }, + "another": { + Key: "another", + Service: "another", + Status: ServiceStopped, + Children: map[string]*Vertex{}, + Parents: map[string]*Vertex{ + "test": {}, + }, + }, + }, + }, + { + desc: "builds graph with multiple dependency levels", + services: types.Services{ + { + Name: "test", + DependsOn: types.DependsOnConfig{ + "another": types.ServiceDependency{}, + }, + }, + { + Name: "another", + DependsOn: types.DependsOnConfig{ + "another_dep": types.ServiceDependency{}, + }, + }, + { + Name: "another_dep", + DependsOn: types.DependsOnConfig{}, + }, + }, + expectedVertices: map[string]*Vertex{ + "test": { + Key: "test", + Service: "test", + Status: ServiceStopped, + Children: map[string]*Vertex{ + "another": {}, + }, + Parents: map[string]*Vertex{}, + }, + "another": { + Key: "another", + Service: "another", + Status: ServiceStopped, + Children: map[string]*Vertex{ + "another_dep": {}, + }, + Parents: map[string]*Vertex{ + "test": {}, + }, + }, + "another_dep": { + Key: "another_dep", + Service: "another_dep", + Status: ServiceStopped, + Children: map[string]*Vertex{}, + Parents: map[string]*Vertex{ + "another": {}, + }, + }, + }, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + project := types.Project{ + Services: tC.services, + } + + graph, err := NewGraph(project.Services, ServiceStopped) + assert.NilError(t, err, fmt.Sprintf("failed to build graph for: %s", tC.desc)) + + for k, vertex := range graph.Vertices { + expected, ok := tC.expectedVertices[k] + assert.Equal(t, true, ok) + assert.Equal(t, true, isVertexEqual(*expected, *vertex)) + } + }) + } +} + +func isVertexEqual(a, b Vertex) bool { + childrenEquality := true + for c := range a.Children { + if _, ok := b.Children[c]; !ok { + childrenEquality = false + } + } + parentEquality := true + for p := range a.Parents { + if _, ok := b.Parents[p]; !ok { + parentEquality = false + } + } + return a.Key == b.Key && + a.Service == b.Service && + childrenEquality && + parentEquality +} From a7cc406187161a99f406220d0337b58dca208b78 Mon Sep 17 00:00:00 2001 From: Laura Brehm Date: Thu, 8 Sep 2022 12:47:05 -0400 Subject: [PATCH 12/18] Cleanup E2E tests Signed-off-by: Laura Brehm --- pkg/e2e/start_stop_test.go | 75 +++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/pkg/e2e/start_stop_test.go b/pkg/e2e/start_stop_test.go index 892142c327c..9c19c48db0c 100644 --- a/pkg/e2e/start_stop_test.go +++ b/pkg/e2e/start_stop_test.go @@ -222,46 +222,53 @@ func TestStopAlreadyStopped(t *testing.T) { } func TestStartStopMultipleServices(t *testing.T) { - cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple")) + cli := NewParallelCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple", + "COMPOSE_FILE=./fixtures/start-stop/compose.yaml")) t.Cleanup(func() { cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0") }) - t.Run("starts/stops multiple services", func(t *testing.T) { - cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "up", "-d", "--wait") - - res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "stop", "simple", "another") - services := []string{"simple", "another"} - for _, svc := range services { - stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) - assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), - fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) - } - - res = cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/compose.yaml", "start", "simple", "another") - for _, svc := range services { - startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) - assert.Assert(t, strings.Contains(res.Stderr(), startMsg), - fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) - } - }) + cli.RunDockerComposeCmd(t, "up", "-d", "--wait") + + res := cli.RunDockerComposeCmd(t, "stop", "simple", "another") + services := []string{"simple", "another"} + for _, svc := range services { + stopMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Stopped", svc) + assert.Assert(t, strings.Contains(res.Stderr(), stopMsg), + fmt.Sprintf("Missing stop message for %s\n%s", svc, res.Combined())) + } + + res = cli.RunDockerComposeCmd(t, "start", "simple", "another") + for _, svc := range services { + startMsg := fmt.Sprintf("Container e2e-start-stop-svc-multiple-%s-1 Started", svc) + assert.Assert(t, strings.Contains(res.Stderr(), startMsg), + fmt.Sprintf("Missing start message for %s\n%s", svc, res.Combined())) + } +} - t.Run("starts one service out of many, and it's dependencies", func(t *testing.T) { - cli.RunDockerComposeCmd(t, "down") - cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "create", "desired") - - res := cli.RunDockerComposeCmd(t, "-f", "./fixtures/start-stop/start-stop-deps.yaml", "start", "desired") - desiredServices := []string{"desired", "dep_1", "dep_2"} - for _, s := range desiredServices { - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-desired-1 Started"), - fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) - } - undesiredServices := []string{"another", "another_2"} - for _, s := range undesiredServices { - assert.Assert(t, strings.Contains(res.Combined(), "Container e2e-start-stop-svc-multiple-dep_2-1 Started"), - fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) - } +func TestStartSingleServiceAndDependency(t *testing.T) { + cli := NewParallelCLI(t, WithEnv( + "COMPOSE_PROJECT_NAME=e2e-start-single-deps", + "COMPOSE_FILE=./fixtures/start-stop/start-stop-deps.yaml")) + t.Cleanup(func() { + cli.RunDockerComposeCmd(t, "down", "--remove-orphans", "-v", "-t", "0") }) + + cli.RunDockerComposeCmd(t, "create", "desired") + + res := cli.RunDockerComposeCmd(t, "start", "desired") + desiredServices := []string{"desired", "dep_1", "dep_2"} + for _, s := range desiredServices { + startMsg := fmt.Sprintf("Container e2e-start-single-deps-%s-1 Started", s) + assert.Assert(t, strings.Contains(res.Combined(), startMsg), + fmt.Sprintf("Missing start message for service: %s\n%s", s, res.Combined())) + } + undesiredServices := []string{"another", "another_2"} + for _, s := range undesiredServices { + assert.Assert(t, !strings.Contains(res.Combined(), s), + fmt.Sprintf("Shouldn't have message for service: %s\n%s", s, res.Combined())) + } } func TestStartStopMultipleFiles(t *testing.T) { From 7a8d157871aa0095d73aefb542a1bda2a58a3efe Mon Sep 17 00:00:00 2001 From: Lucas Berg <55436804+BergLucas@users.noreply.github.com> Date: Thu, 8 Sep 2022 22:25:23 +0200 Subject: [PATCH 13/18] convert: do not escape $ into $$ when using the --no-interpolate option (#9703) Signed-off-by: Lucas Berg Signed-off-by: Ulysses Souza Co-authored-by: Ulysses Souza --- cmd/compose/convert.go | 19 ++++++++++++---- pkg/compose/compose.go | 19 ++-------------- pkg/e2e/compose_test.go | 22 +++++++++++++++++++ .../compose-interpolate.yaml | 5 +++++ 4 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml diff --git a/cmd/compose/convert.go b/cmd/compose/convert.go index 79692620e76..b1adecc98b2 100644 --- a/cmd/compose/convert.go +++ b/cmd/compose/convert.go @@ -18,6 +18,7 @@ package compose import ( "bufio" + "bytes" "context" "fmt" "io" @@ -112,7 +113,7 @@ func convertCommand(p *projectOptions, backend api.Service) *cobra.Command { } func runConvert(ctx context.Context, backend api.Service, opts convertOptions, services []string) error { - var json []byte + var content []byte project, err := opts.toProject(services, cli.WithInterpolation(!opts.noInterpolate), cli.WithResolvedPaths(true), @@ -136,7 +137,7 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s } } - json, err = backend.Convert(ctx, project, api.ConvertOptions{ + content, err = backend.Convert(ctx, project, api.ConvertOptions{ Format: opts.Format, Output: opts.Output, }) @@ -144,19 +145,23 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s return err } + if !opts.noInterpolate { + content = escapeDollarSign(content) + } + if opts.quiet { return nil } var out io.Writer = os.Stdout - if opts.Output != "" && len(json) > 0 { + if opts.Output != "" && len(content) > 0 { file, err := os.Create(opts.Output) if err != nil { return err } out = bufio.NewWriter(file) } - _, err = fmt.Fprint(out, string(json)) + _, err = fmt.Fprint(out, string(content)) return err } @@ -237,3 +242,9 @@ func runConfigImages(opts convertOptions, services []string) error { } return nil } + +func escapeDollarSign(marshal []byte) []byte { + dollar := []byte{'$'} + escDollar := []byte{'$', '$'} + return bytes.ReplaceAll(marshal, dollar, escDollar) +} diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index 603e7057a59..ef5d58780e1 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -17,7 +17,6 @@ package compose import ( - "bytes" "context" "encoding/json" "fmt" @@ -95,28 +94,14 @@ func getContainerNameWithoutProject(c moby.Container) string { func (s *composeService) Convert(ctx context.Context, project *types.Project, options api.ConvertOptions) ([]byte, error) { switch options.Format { case "json": - marshal, err := json.MarshalIndent(project, "", " ") - if err != nil { - return nil, err - } - return escapeDollarSign(marshal), nil + return json.MarshalIndent(project, "", " ") case "yaml": - marshal, err := yaml.Marshal(project) - if err != nil { - return nil, err - } - return escapeDollarSign(marshal), nil + return yaml.Marshal(project) default: return nil, fmt.Errorf("unsupported format %q", options) } } -func escapeDollarSign(marshal []byte) []byte { - dollar := []byte{'$'} - escDollar := []byte{'$', '$'} - return bytes.ReplaceAll(marshal, dollar, escDollar) -} - // projectFromName builds a types.Project based on actual resources with compose labels set func (s *composeService) projectFromName(containers Containers, projectName string, services ...string) (*types.Project, error) { project := &types.Project{ diff --git a/pkg/e2e/compose_test.go b/pkg/e2e/compose_test.go index 668bb7747c1..de5d3c029a5 100644 --- a/pkg/e2e/compose_test.go +++ b/pkg/e2e/compose_test.go @@ -234,3 +234,25 @@ networks: name: compose-e2e-convert_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) }) } + +func TestConvertInterpolate(t *testing.T) { + const projectName = "compose-e2e-convert-interpolate" + c := NewParallelCLI(t) + + wd, err := os.Getwd() + assert.NilError(t, err) + + t.Run("convert", func(t *testing.T) { + res := c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-build-test/compose-interpolate.yaml", "-p", projectName, "convert", "--no-interpolate") + res.Assert(t, icmd.Expected{Out: fmt.Sprintf(`services: + nginx: + build: + context: %s + dockerfile: ${MYVAR} + networks: + default: null +networks: + default: + name: compose-e2e-convert-interpolate_default`, filepath.Join(wd, "fixtures", "simple-build-test", "nginx-build")), ExitCode: 0}) + }) +} diff --git a/pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml b/pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml new file mode 100644 index 00000000000..57d092a9909 --- /dev/null +++ b/pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml @@ -0,0 +1,5 @@ +services: + nginx: + build: + context: nginx-build + dockerfile: ${MYVAR} From 61845dd7816cec9d42c715c8b157f88dfe78fd54 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Thu, 8 Sep 2022 16:26:00 -0400 Subject: [PATCH 14/18] logs: filter to services from current Compose file (#9811) * logs: filter to services from current Compose file When using the file model, only attach to services referenced in the active Compose file. For example, let's say you have `compose-base.yaml` and `compose.yaml`, where the former only has a subset of the services but are both run as part of the same named project. Project based command: ``` docker compose -p myproj logs ``` This should return logs for active services based on the project name, regardless of Compose file state on disk. File based command: ``` docker compose --file compose-base.yaml logs ``` This should return logs for ONLY services that are defined in `compose-base.yaml`. Any other services are considered 'orphaned' within the context of the command and should be ignored. See also #9705. Fixes #9801. Signed-off-by: Milas Bowman --- cmd/compose/logs.go | 5 +- pkg/api/api.go | 9 +- pkg/compose/convergence_test.go | 29 +++-- pkg/compose/down_test.go | 24 +++- pkg/compose/kill_test.go | 16 ++- pkg/compose/logs.go | 21 +++- pkg/compose/logs_test.go | 204 ++++++++++++++++++++++++++++++++ pkg/compose/ps_test.go | 4 +- pkg/compose/stop_test.go | 4 +- 9 files changed, 290 insertions(+), 26 deletions(-) create mode 100644 pkg/compose/logs_test.go diff --git a/cmd/compose/logs.go b/cmd/compose/logs.go index ffe3dc2e947..a9fd7255a55 100644 --- a/cmd/compose/logs.go +++ b/cmd/compose/logs.go @@ -63,12 +63,13 @@ func logsCommand(p *projectOptions, backend api.Service) *cobra.Command { } func runLogs(ctx context.Context, backend api.Service, opts logsOptions, services []string) error { - projectName, err := opts.toProjectName() + project, name, err := opts.projectOrName() if err != nil { return err } consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix) - return backend.Logs(ctx, projectName, consumer, api.LogOptions{ + return backend.Logs(ctx, name, consumer, api.LogOptions{ + Project: project, Services: services, Follow: opts.follow, Tail: opts.tail, diff --git a/pkg/api/api.go b/pkg/api/api.go index 0cd7e609dca..2648bf0acb1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -380,6 +380,7 @@ type ServiceStatus struct { // LogOptions defines optional parameters for the `Log` API type LogOptions struct { + Project *types.Project Services []string Tail string Since string @@ -431,7 +432,7 @@ type Stack struct { // LogConsumer is a callback to process log messages from services type LogConsumer interface { - Log(service, container, message string) + Log(containerName, service, message string) Status(container, msg string) Register(container string) } @@ -441,7 +442,11 @@ type ContainerEventListener func(event ContainerEvent) // ContainerEvent notify an event has been collected on source container implementing Service type ContainerEvent struct { - Type int + Type int + // Container is the name of the container _without the project prefix_. + // + // This is only suitable for display purposes within Compose, as it's + // not guaranteed to be unique across services. Container string Service string Line string diff --git a/pkg/compose/convergence_test.go b/pkg/compose/convergence_test.go index bd41db30a5d..a27f5958966 100644 --- a/pkg/compose/convergence_test.go +++ b/pkg/compose/convergence_test.go @@ -23,12 +23,13 @@ import ( "testing" "github.com/compose-spec/compose-go/types" - "github.com/docker/compose/v2/pkg/api" - "github.com/docker/compose/v2/pkg/mocks" moby "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/golang/mock/gomock" "gotest.tools/assert" + + "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/mocks" ) func TestContainerName(t *testing.T) { @@ -77,7 +78,9 @@ func TestServiceLinks(t *testing.T) { apiClient := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(apiClient).AnyTimes() s.Links = []string{"db"} @@ -99,7 +102,9 @@ func TestServiceLinks(t *testing.T) { defer mockCtrl.Finish() apiClient := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(apiClient).AnyTimes() s.Links = []string{"db:db"} @@ -121,7 +126,9 @@ func TestServiceLinks(t *testing.T) { defer mockCtrl.Finish() apiClient := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(apiClient).AnyTimes() s.Links = []string{"db:dbname"} @@ -143,7 +150,9 @@ func TestServiceLinks(t *testing.T) { defer mockCtrl.Finish() apiClient := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(apiClient).AnyTimes() s.Links = []string{"db:dbname"} @@ -169,7 +178,9 @@ func TestServiceLinks(t *testing.T) { defer mockCtrl.Finish() apiClient := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(apiClient).AnyTimes() s.Links = []string{} @@ -203,7 +214,9 @@ func TestWaitDependencies(t *testing.T) { apiClient := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(apiClient).AnyTimes() t.Run("should skip dependencies with scale 0", func(t *testing.T) { diff --git a/pkg/compose/down_test.go b/pkg/compose/down_test.go index e5d527fdabe..e0d88d59a9d 100644 --- a/pkg/compose/down_test.go +++ b/pkg/compose/down_test.go @@ -37,7 +37,9 @@ func TestDown(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return( @@ -85,7 +87,9 @@ func TestDownRemoveOrphans(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(true)).Return( @@ -122,7 +126,9 @@ func TestDownRemoveVolumes(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return( @@ -149,7 +155,9 @@ func TestDownRemoveImageLocal(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() container := testContainer("service1", "123", false) @@ -180,7 +188,9 @@ func TestDownRemoveImageLocalNoLabel(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() container := testContainer("service1", "123", false) @@ -208,7 +218,9 @@ func TestDownRemoveImageAll(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt(false)).Return( diff --git a/pkg/compose/kill_test.go b/pkg/compose/kill_test.go index b5cc8176f82..a50308a4d17 100644 --- a/pkg/compose/kill_test.go +++ b/pkg/compose/kill_test.go @@ -35,15 +35,15 @@ import ( const testProject = "testProject" -var tested = composeService{} - func TestKillAll(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() name := strings.ToLower(testProject) @@ -74,7 +74,9 @@ func TestKillSignal(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() name := strings.ToLower(testProject) @@ -97,9 +99,13 @@ func TestKillSignal(t *testing.T) { } func testContainer(service string, id string, oneOff bool) moby.Container { + // canonical docker names in the API start with a leading slash, some + // parts of Compose code will attempt to strip this off, so make sure + // it's consistently present + name := "/" + strings.TrimPrefix(id, "/") return moby.Container{ ID: id, - Names: []string{id}, + Names: []string{name}, Labels: containerLabels(service, oneOff), } } diff --git a/pkg/compose/logs.go b/pkg/compose/logs.go index e8a16e78022..ce2f0cf96a3 100644 --- a/pkg/compose/logs.go +++ b/pkg/compose/logs.go @@ -29,13 +29,32 @@ import ( "github.com/docker/compose/v2/pkg/utils" ) -func (s *composeService) Logs(ctx context.Context, projectName string, consumer api.LogConsumer, options api.LogOptions) error { +func (s *composeService) Logs( + ctx context.Context, + projectName string, + consumer api.LogConsumer, + options api.LogOptions, +) error { projectName = strings.ToLower(projectName) + containers, err := s.getContainers(ctx, projectName, oneOffExclude, true, options.Services...) if err != nil { return err } + project := options.Project + if project == nil { + project, err = s.getProjectWithResources(ctx, containers, projectName) + if err != nil { + return err + } + } + + if len(options.Services) == 0 { + options.Services = project.ServiceNames() + } + + containers = containers.filter(isService(options.Services...)) eg, ctx := errgroup.WithContext(ctx) for _, c := range containers { c := c diff --git a/pkg/compose/logs_test.go b/pkg/compose/logs_test.go new file mode 100644 index 00000000000..2bd05737c4c --- /dev/null +++ b/pkg/compose/logs_test.go @@ -0,0 +1,204 @@ +/* + Copyright 2022 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 ( + "context" + "io" + "strings" + "sync" + "testing" + + "github.com/compose-spec/compose-go/types" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/pkg/stdcopy" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + compose "github.com/docker/compose/v2/pkg/api" + "github.com/docker/compose/v2/pkg/mocks" +) + +func TestComposeService_Logs_Demux(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + api := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := composeService{ + dockerCli: cli, + } + cli.EXPECT().Client().Return(api).AnyTimes() + + name := strings.ToLower(testProject) + + ctx := context.Background() + api.EXPECT().ContainerList(ctx, moby.ContainerListOptions{ + All: true, + Filters: filters.NewArgs(oneOffFilter(false), projectFilter(name)), + }).Return( + []moby.Container{ + testContainer("service", "c", false), + }, + nil, + ) + + api.EXPECT(). + ContainerInspect(anyCancellableContext(), "c"). + Return(moby.ContainerJSON{ + ContainerJSONBase: &moby.ContainerJSONBase{ID: "c"}, + Config: &container.Config{Tty: false}, + }, nil) + c1Reader, c1Writer := io.Pipe() + t.Cleanup(func() { + _ = c1Reader.Close() + _ = c1Writer.Close() + }) + c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout) + c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr) + go func() { + _, err := c1Stdout.Write([]byte("hello stdout\n")) + assert.NoError(t, err, "Writing to fake stdout") + _, err = c1Stderr.Write([]byte("hello stderr\n")) + assert.NoError(t, err, "Writing to fake stderr") + _ = c1Writer.Close() + }() + api.EXPECT().ContainerLogs(anyCancellableContext(), "c", gomock.Any()). + Return(c1Reader, nil) + + opts := compose.LogOptions{ + Project: &types.Project{ + Services: types.Services{ + {Name: "service"}, + }, + }, + } + + consumer := &testLogConsumer{} + err := tested.Logs(ctx, name, consumer, opts) + require.NoError(t, err) + + require.Equal( + t, + []string{"hello stdout", "hello stderr"}, + consumer.LogsForContainer("service", "c"), + ) +} + +// TestComposeService_Logs_ServiceFiltering ensures that we do not include +// logs from out-of-scope services based on the Compose file vs actual state. +// +// NOTE(milas): This test exists because each method is currently duplicating +// a lot of the project/service filtering logic. We should consider moving it +// to an earlier point in the loading process, at which point this test could +// safely be removed. +func TestComposeService_Logs_ServiceFiltering(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + api := mocks.NewMockAPIClient(mockCtrl) + cli := mocks.NewMockCli(mockCtrl) + tested := composeService{ + dockerCli: cli, + } + cli.EXPECT().Client().Return(api).AnyTimes() + + name := strings.ToLower(testProject) + + ctx := context.Background() + api.EXPECT().ContainerList(ctx, moby.ContainerListOptions{ + All: true, + Filters: filters.NewArgs(oneOffFilter(false), projectFilter(name)), + }).Return( + []moby.Container{ + testContainer("serviceA", "c1", false), + testContainer("serviceA", "c2", false), + // serviceB will be filtered out by the project definition to + // ensure we ignore "orphan" containers + testContainer("serviceB", "c3", false), + testContainer("serviceC", "c4", false), + }, + nil, + ) + + for _, id := range []string{"c1", "c2", "c4"} { + id := id + api.EXPECT(). + ContainerInspect(anyCancellableContext(), id). + Return( + moby.ContainerJSON{ + ContainerJSONBase: &moby.ContainerJSONBase{ID: id}, + Config: &container.Config{Tty: true}, + }, + nil, + ) + api.EXPECT().ContainerLogs(anyCancellableContext(), id, gomock.Any()). + Return(io.NopCloser(strings.NewReader("hello "+id+"\n")), nil). + Times(1) + } + + // this simulates passing `--filename` with a Compose file that does NOT + // reference `serviceB` even though it has running services for this proj + proj := &types.Project{ + Services: types.Services{ + {Name: "serviceA"}, + {Name: "serviceC"}, + }, + } + consumer := &testLogConsumer{} + opts := compose.LogOptions{ + Project: proj, + } + err := tested.Logs(ctx, name, consumer, opts) + require.NoError(t, err) + + require.Equal(t, []string{"hello c1"}, consumer.LogsForContainer("serviceA", "c1")) + require.Equal(t, []string{"hello c2"}, consumer.LogsForContainer("serviceA", "c2")) + require.Empty(t, consumer.LogsForContainer("serviceB", "c3")) + require.Equal(t, []string{"hello c4"}, consumer.LogsForContainer("serviceC", "c4")) +} + +type testLogConsumer struct { + mu sync.Mutex + // logs is keyed by service, then container; values are log lines + logs map[string]map[string][]string +} + +func (l *testLogConsumer) Log(containerName, service, message string) { + l.mu.Lock() + defer l.mu.Unlock() + if l.logs == nil { + l.logs = make(map[string]map[string][]string) + } + if l.logs[service] == nil { + l.logs[service] = make(map[string][]string) + } + l.logs[service][containerName] = append(l.logs[service][containerName], message) +} + +func (l *testLogConsumer) Status(containerName, msg string) {} + +func (l *testLogConsumer) Register(containerName string) {} + +func (l *testLogConsumer) LogsForContainer(svc string, containerName string) []string { + l.mu.Lock() + defer l.mu.Unlock() + return l.logs[svc][containerName] +} diff --git a/pkg/compose/ps_test.go b/pkg/compose/ps_test.go index 669b09e21cc..a7e1ac9bf75 100644 --- a/pkg/compose/ps_test.go +++ b/pkg/compose/ps_test.go @@ -38,7 +38,9 @@ func TestPs(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() ctx := context.Background() diff --git a/pkg/compose/stop_test.go b/pkg/compose/stop_test.go index 97a83356ea7..0e61c295ecc 100644 --- a/pkg/compose/stop_test.go +++ b/pkg/compose/stop_test.go @@ -38,7 +38,9 @@ func TestStopTimeout(t *testing.T) { api := mocks.NewMockAPIClient(mockCtrl) cli := mocks.NewMockCli(mockCtrl) - tested.dockerCli = cli + tested := composeService{ + dockerCli: cli, + } cli.EXPECT().Client().Return(api).AnyTimes() ctx := context.Background() From e413c2137a44cd65f698f91287dc124e29ed1909 Mon Sep 17 00:00:00 2001 From: Taha-Chaudhry <46199675+Taha-Chaudhry@users.noreply.github.com> Date: Sun, 11 Sep 2022 17:28:35 +0300 Subject: [PATCH 15/18] Update README.md Grammar corrections Signed-off-by: Taha-Chaudhry <46199675+Taha-Chaudhry@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2007d998791..43af9f6df59 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,12 @@ You can download Docker Compose binaries from the Rename the relevant binary for your OS to `docker-compose` and copy it to `$HOME/.docker/cli-plugins` -Or copy it into one of these folders for installing it system-wide: +Or copy it into one of these folders to install it system-wide: * `/usr/local/lib/docker/cli-plugins` OR `/usr/local/libexec/docker/cli-plugins` * `/usr/lib/docker/cli-plugins` OR `/usr/libexec/docker/cli-plugins` -(might require to make the downloaded file executable with `chmod +x`) +(might require making the downloaded file executable with `chmod +x`) Quick Start From ab984d91af9bc56879e50bd8dbc9de4cafe1197c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 10:27:14 +0100 Subject: [PATCH 16/18] build(deps): bump github.com/AlecAivazis/survey/v2 from 2.3.5 to 2.3.6 (#9830) Bumps [github.com/AlecAivazis/survey/v2](https://github.com/AlecAivazis/survey) from 2.3.5 to 2.3.6. - [Release notes](https://github.com/AlecAivazis/survey/releases) - [Commits](https://github.com/AlecAivazis/survey/compare/v2.3.5...v2.3.6) --- updated-dependencies: - dependency-name: github.com/AlecAivazis/survey/v2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1efee8bc4e6..8981bee9a84 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/docker/compose/v2 go 1.18 require ( - github.com/AlecAivazis/survey/v2 v2.3.5 + github.com/AlecAivazis/survey/v2 v2.3.6 github.com/buger/goterm v1.0.4 github.com/cnabio/cnab-go v0.24.1-0.20220907172316-1ca5c8721bf7 github.com/cnabio/cnab-to-oci v0.3.7 diff --git a/go.sum b/go.sum index 54f7502a847..80527885faf 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/AkihiroSuda/containerd-fuse-overlayfs v1.0.0/go.mod h1:0mMDvQFeLbbn1Wy8P2j3hwFhqBq+FKn8OZPno8WLmp8= -github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= -github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= +github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8S9ziyw= +github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= github.com/Azure/azure-amqp-common-go/v2 v2.1.0/go.mod h1:R8rea+gJRuJR6QxTir/XuEd+YuKoUiazDC/N96FiDEU= github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= From de1d969c3707fee0461d0fa26e2de4d5f2e3e0b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Sep 2022 09:42:35 +0000 Subject: [PATCH 17/18] build(deps): bump go.opentelemetry.io/otel from 1.9.0 to 1.10.0 Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 8981bee9a84..739cb435d88 100644 --- a/go.mod +++ b/go.mod @@ -101,12 +101,12 @@ 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.9.0 + go.opentelemetry.io/otel v1.10.0 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 go.opentelemetry.io/otel/sdk v1.4.1 // indirect - go.opentelemetry.io/otel/trace v1.9.0 // indirect + go.opentelemetry.io/otel/trace v1.10.0 // indirect go.opentelemetry.io/proto/otlp v0.12.0 // indirect golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect diff --git a/go.sum b/go.sum index 80527885faf..45292169b5a 100644 --- a/go.sum +++ b/go.sum @@ -1471,8 +1471,8 @@ go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzox go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= go.opentelemetry.io/otel v1.4.0/go.mod h1:jeAqMFKy2uLIxCtKxoFj0FAL5zAPKQagc3+GtBWakzk= go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= -go.opentelemetry.io/otel v1.9.0 h1:8WZNQFIB2a71LnANS9JeyidJKKGOOremcUtb/OtHISw= -go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= +go.opentelemetry.io/otel v1.10.0 h1:Y7DTJMR6zs1xkS/upamJYk0SxxN4C9AqRd77jmZnyY4= +go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ= 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= @@ -1503,8 +1503,8 @@ go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16g go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= go.opentelemetry.io/otel/trace v1.4.0/go.mod h1:uc3eRsqDfWs9R7b92xbQbU42/eTNz4N+gLP8qJCi4aE= go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= -go.opentelemetry.io/otel/trace v1.9.0 h1:oZaCNJUjWcg60VXWee8lJKlqhPbXAPB51URuR47pQYc= -go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= +go.opentelemetry.io/otel/trace v1.10.0 h1:npQMbR8o7mum8uF95yFbOEJffhs1sbCOfDh8zAJiH5E= +go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= go.opentelemetry.io/proto/otlp v0.12.0 h1:CMJ/3Wp7iOWES+CYLfnBv+DVmPbB+kmy9PJ92XvlR6c= From d05f5f5fa71d5d0c5d94654f3f822dcba523d606 Mon Sep 17 00:00:00 2001 From: Risky Feryansyah <36788585+RiskyFeryansyahP@users.noreply.github.com> Date: Tue, 13 Sep 2022 20:38:13 +0700 Subject: [PATCH 18/18] pull: improve output for services with both image+build (#9829) When an image pull fails but the service has a `build` section, it will be built, so it's not an unrecoverable error. It's now logged as a warning - in situations where the image will _never_ exist in a registry, `pull_policy: never` can & should be used, which will prevent the error and avoid unnecessary pull attempts. Signed-off-by: Risky Feryansyah Pribadi --- pkg/compose/pull.go | 12 ++++++++++++ pkg/progress/event.go | 2 ++ pkg/progress/tty.go | 5 ++++- pkg/progress/tty_test.go | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/pkg/compose/pull.go b/pkg/compose/pull.go index fbeacacec66..88cfefbf6ab 100644 --- a/pkg/compose/pull.go +++ b/pkg/compose/pull.go @@ -181,6 +181,18 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser RegistryAuth: base64.URLEncoding.EncodeToString(buf), Platform: service.Platform, }) + + // check if has error and the service has a build section + // then the status should be warning instead of error + if err != nil && service.Build != nil { + w.Event(progress.Event{ + ID: service.Name, + Status: progress.Warning, + Text: "Warning", + }) + return "", WrapCategorisedComposeError(err, PullFailure) + } + if err != nil { w.Event(progress.Event{ ID: service.Name, diff --git a/pkg/progress/event.go b/pkg/progress/event.go index 0ead54dd417..8603410e90d 100644 --- a/pkg/progress/event.go +++ b/pkg/progress/event.go @@ -28,6 +28,8 @@ const ( Done // Error means that the current task has errored Error + // Warning means that the current task has warning + Warning ) // Event represents a progress event. diff --git a/pkg/progress/tty.go b/pkg/progress/tty.go index 09338fb8372..8729571aef9 100644 --- a/pkg/progress/tty.go +++ b/pkg/progress/tty.go @@ -75,7 +75,7 @@ func (w *ttyWriter) Event(e Event) { if _, ok := w.events[e.ID]; ok { last := w.events[e.ID] switch e.Status { - case Done, Error: + case Done, Error, Warning: if last.Status != e.Status { last.stop() } @@ -222,6 +222,9 @@ func lineText(event Event, pad string, terminalWidth, statusPadding int, color b if event.Status == Error { color = aec.RedF } + if event.Status == Warning { + color = aec.YellowF + } return aec.Apply(o, color) } diff --git a/pkg/progress/tty_test.go b/pkg/progress/tty_test.go index f9b4071d1ad..e9e0c7e2dc8 100644 --- a/pkg/progress/tty_test.go +++ b/pkg/progress/tty_test.go @@ -54,6 +54,10 @@ func TestLineText(t *testing.T) { ev.Status = Error out = lineText(ev, "", 50, lineWidth, true) assert.Equal(t, out, "\x1b[31m . id Text Status 0.0s\n\x1b[0m") + + ev.Status = Warning + out = lineText(ev, "", 50, lineWidth, true) + assert.Equal(t, out, "\x1b[33m . id Text Status 0.0s\n\x1b[0m") } func TestLineTextSingleEvent(t *testing.T) { @@ -103,3 +107,32 @@ func TestErrorEvent(t *testing.T) { assert.Assert(t, ok) assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second))) } + +func TestWarningEvent(t *testing.T) { + w := &ttyWriter{ + events: map[string]Event{}, + mtx: &sync.Mutex{}, + } + e := Event{ + ID: "id", + Text: "Text", + Status: Working, + StatusText: "Working", + startTime: time.Now(), + spinner: &spinner{ + chars: []string{"."}, + }, + } + // Fire "Working" event and check end time isn't touched + w.Event(e) + event, ok := w.events[e.ID] + assert.Assert(t, ok) + assert.Assert(t, event.endTime.Equal(time.Time{})) + + // Fire "Warning" event and check end time is set + e.Status = Warning + w.Event(e) + event, ok = w.events[e.ID] + assert.Assert(t, ok) + assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second))) +}