Explorar o código

Merge remote-tracking branch 'upstream/v2' into down-image-rm

Milas Bowman %!s(int64=3) %!d(string=hai) anos
pai
achega
b49b9ffe7e
Modificáronse 46 ficheiros con 1368 adicións e 130 borrados
  1. 2 2
      .github/workflows/ci.yml
  2. 4 3
      .golangci.yml
  3. 2 2
      Dockerfile
  4. 2 2
      README.md
  5. 12 0
      cmd/compose/compose.go
  6. 15 4
      cmd/compose/convert.go
  7. 3 2
      cmd/compose/logs.go
  8. 1 0
      cmd/compose/start.go
  9. 35 0
      cmd/compose/tracing.go
  10. 1 1
      docker-bake.hcl
  11. 21 11
      go.mod
  12. 31 14
      go.sum
  13. 9 2
      pkg/api/api.go
  14. 56 15
      pkg/compose/build.go
  15. 204 23
      pkg/compose/build_buildkit.go
  16. 4 0
      pkg/compose/build_classic.go
  17. 2 17
      pkg/compose/compose.go
  18. 21 8
      pkg/compose/convergence_test.go
  19. 17 10
      pkg/compose/dependencies.go
  20. 180 0
      pkg/compose/dependencies_test.go
  21. 15 5
      pkg/compose/down_test.go
  22. 11 5
      pkg/compose/kill_test.go
  23. 20 1
      pkg/compose/logs.go
  24. 204 0
      pkg/compose/logs_test.go
  25. 3 1
      pkg/compose/ps_test.go
  26. 12 0
      pkg/compose/pull.go
  27. 7 0
      pkg/compose/start.go
  28. 3 1
      pkg/compose/stop_test.go
  29. 102 0
      pkg/e2e/build_test.go
  30. 22 0
      pkg/e2e/compose_test.go
  31. 22 0
      pkg/e2e/fixtures/build-test/platforms/Dockerfile
  32. 23 0
      pkg/e2e/fixtures/build-test/platforms/compose-multiple-platform-builds.yaml
  33. 9 0
      pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml
  34. 8 0
      pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml
  35. 9 0
      pkg/e2e/fixtures/build-test/platforms/compose.yaml
  36. 22 0
      pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile
  37. 22 0
      pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile
  38. 22 0
      pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile
  39. 5 0
      pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml
  40. 17 0
      pkg/e2e/fixtures/start-stop/start-stop-deps.yaml
  41. 24 0
      pkg/e2e/start_stop_test.go
  42. 2 0
      pkg/progress/event.go
  43. 4 1
      pkg/progress/tty.go
  44. 33 0
      pkg/progress/tty_test.go
  45. 30 0
      pkg/utils/slices.go
  46. 95 0
      pkg/utils/slices_test.go

+ 2 - 2
.github/workflows/ci.yml

@@ -19,7 +19,6 @@ on:
         default: "false"
 
 env:
-  GO_VERSION: "1.18.5" # for non sandboxed e2e tests
   DESTDIR: "./bin"
   DOCKER_CLI_VERSION: "20.10.17"
 
@@ -143,7 +142,8 @@ jobs:
         name: Set up Go
         uses: actions/setup-go@v3
         with:
-          go-version: ${{ env.GO_VERSION }}
+          go-version-file: 'go.mod'
+          check-latest: true
           cache: true
       -
         name: Setup docker CLI

+ 4 - 3
.golangci.yml

@@ -5,7 +5,6 @@ linters:
   enable-all: false
   disable-all: true
   enable:
-    - deadcode
     - depguard
     - errcheck
     - gocritic
@@ -21,13 +20,15 @@ linters:
     - nakedret
     - nolintlint
     - staticcheck
-    - structcheck
     - typecheck
     - unconvert
     - unparam
     - unused
-    - varcheck
 linters-settings:
+  revive:
+    rules:
+      - name: package-comments
+        disabled: true
   depguard:
     list-type: denylist
     include-go-root: true

+ 2 - 2
Dockerfile

@@ -15,9 +15,9 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-ARG GO_VERSION=1.18.5
+ARG GO_VERSION=1.19.1
 ARG XX_VERSION=1.1.2
-ARG GOLANGCI_LINT_VERSION=v1.47.3
+ARG GOLANGCI_LINT_VERSION=v1.49.0
 ARG ADDLICENSE_VERSION=v1.0.0
 
 ARG BUILD_TAGS="e2e,kube"

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

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

+ 15 - 4
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)
+}

+ 3 - 2
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,

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

+ 35 - 0
cmd/compose/tracing.go

@@ -0,0 +1,35 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package compose
+
+import (
+	"github.com/moby/buildkit/util/tracing/detect"
+	"go.opentelemetry.io/otel"
+
+	_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
+	_ "github.com/moby/buildkit/util/tracing/env"              //nolint:blank-imports
+)
+
+func init() {
+	detect.ServiceName = "compose"
+	// do not log tracing errors to stdio
+	otel.SetErrorHandler(skipErrors{})
+}
+
+type skipErrors struct{}
+
+func (skipErrors) Handle(err error) {}

+ 1 - 1
docker-bake.hcl

@@ -13,7 +13,7 @@
 // limitations under the License.
 
 variable "GO_VERSION" {
-  default = "1.18.5"
+  default = "1.19.1"
 }
 
 variable "BUILD_TAGS" {

+ 21 - 11
go.mod

@@ -1,15 +1,16 @@
 module github.com/docker/compose/v2
 
-go 1.18
+go 1.19
 
 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
 	github.com/compose-spec/compose-go v1.5.0
 	github.com/containerd/console v1.0.3
 	github.com/containerd/containerd v1.6.8
-	github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f
+	github.com/distribution/distribution/v3 v3.0.0-20220902125104-0122d7ddaec0
 	github.com/docker/buildx v0.8.2 // when updating, also update the replace rules accordingly
 	github.com/docker/cli v20.10.17+incompatible
 	github.com/docker/cli-docs-tool v0.5.0
@@ -22,7 +23,7 @@ require (
 	github.com/mattn/go-isatty v0.0.16
 	github.com/mattn/go-shellwords v1.0.12
 	github.com/moby/buildkit v0.10.4
-	github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6
+	github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae
 	github.com/morikuni/aec v1.0.0
 	github.com/opencontainers/go-digest v1.0.0
 	github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799
@@ -32,7 +33,7 @@ require (
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.0
 	github.com/theupdateframework/notary v0.7.0
-	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4
+	golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
 	gopkg.in/yaml.v2 v2.4.0
 	gotest.tools v2.2.0+incompatible
 	gotest.tools/v3 v3.3.0
@@ -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
@@ -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,15 +101,15 @@ 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.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.4.1 // 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
+	golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
 	golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
 	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // 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,19 @@ 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/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
 	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 (

+ 31 - 14
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=
@@ -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=
@@ -268,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=
@@ -449,8 +450,8 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
 github.com/distribution/distribution/v3 v3.0.0-20210316161203-a01c71e2477e/go.mod h1:xpWTC2KnJMiDLkoawhsPQcXjvwATEBcbq0xevG2YR9M=
-github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f h1:3NCYdjXycNd/Xn/iICZzmxkiDX1e1cjTHjbMAz+wRVk=
-github.com/distribution/distribution/v3 v3.0.0-20220729163034-26163d82560f/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
+github.com/distribution/distribution/v3 v3.0.0-20220902125104-0122d7ddaec0 h1:0UuPq7m6stSY6at1v5PLo0zzYTpailcwjhmkJpgnGBY=
+github.com/distribution/distribution/v3 v3.0.0-20220902125104-0122d7ddaec0/go.mod h1:28YO/VJk9/64+sTGNuYaBjWxrXTPrj0C0XmgTIOjxX4=
 github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
 github.com/docker/buildx v0.8.2 h1:dsd3F0hhmUydFX/KFrvbK81JvlTA4T3Iy0lwDJt4PsU=
 github.com/docker/buildx v0.8.2/go.mod h1:5sMOfNwOmO2jy/MxBL4ySk2LoLIG1tQFu2EU8wbKa34=
@@ -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=
@@ -556,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=
@@ -761,6 +764,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=
@@ -998,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=
@@ -1016,12 +1022,15 @@ 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=
 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=
@@ -1041,8 +1050,9 @@ github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6
 github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
 github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
 github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=
-github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=
 github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
+github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
+github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -1235,6 +1245,7 @@ github.com/securego/gosec v0.0.0-20200401082031-e946c8c39989/go.mod h1:i9l/TNj+y
 github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002 h1:ka9QPuQg2u4LGipiZGsgkg3rJCo4iIUCy75FddM0GRQ=
 github.com/serialx/hashring v0.0.0-20190422032157-8b2912629002/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc=
 github.com/shirou/gopsutil v0.0.0-20190901111213-e4ec7b275ada/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
 github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
@@ -1460,18 +1471,22 @@ 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.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=
+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=
@@ -1488,8 +1503,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.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=
@@ -1498,6 +1514,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=
@@ -1644,8 +1661,8 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
+golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/oauth2 v0.0.0-20180724155351-3d292e4d0cdc/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1679,8 +1696,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc=
+golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

+ 9 - 2
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
@@ -378,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
@@ -429,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)
 }
@@ -439,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

+ 56 - 15
pkg/compose/build.go

@@ -81,6 +81,12 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opti
 				Attrs: map[string]string{"ref": image},
 			})
 		}
+		if len(buildOptions.Platforms) > 1 {
+			buildOptions.Exports = []bclient.ExportEntry{{
+				Type:  "image",
+				Attrs: map[string]string{},
+			}}
+		}
 		opts[imageName] = buildOptions
 	}
 
@@ -161,6 +167,15 @@ func (s *composeService) getBuildOptions(project *types.Project, images map[stri
 			if err != nil {
 				return nil, err
 			}
+			opt.Exports = []bclient.ExportEntry{{
+				Type: "docker",
+				Attrs: map[string]string{
+					"load": "true",
+				},
+			}}
+			if opt.Platforms, err = useDockerDefaultPlatform(project, service.Build.Platforms); err != nil {
+				opt.Platforms = []specs.Platform{}
+			}
 			opts[imageName] = opt
 			continue
 		}
@@ -204,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) {
@@ -213,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,6 +356,28 @@ func addSecretsConfig(project *types.Project, service types.ServiceConfig) (sess
 	return secretsprovider.NewSecretProvider(store), nil
 }
 
+func addPlatforms(project *types.Project, service types.ServiceConfig) ([]specs.Platform, error) {
+	plats, err := useDockerDefaultPlatform(project, service.Build.Platforms)
+	if err != nil {
+		return nil, err
+	}
+
+	if service.Platform != "" && !utils.StringContains(service.Build.Platforms, service.Platform) {
+		return nil, fmt.Errorf("service.platform should be part of the service.build.platforms: %q", service.Platform)
+	}
+
+	for _, buildPlatform := range service.Build.Platforms {
+		p, err := platforms.Parse(buildPlatform)
+		if err != nil {
+			return nil, err
+		}
+		if !utils.Contains(plats, p) {
+			plats = append(plats, p)
+		}
+	}
+	return plats, nil
+}
+
 func getImageBuildLabels(project *types.Project, service types.ServiceConfig) types.Labels {
 	ret := make(types.Labels)
 	if service.Build != nil {
@@ -365,3 +391,18 @@ func getImageBuildLabels(project *types.Project, service types.ServiceConfig) ty
 	ret.Add(api.ServiceLabel, service.Name)
 	return ret
 }
+
+func useDockerDefaultPlatform(project *types.Project, platformList types.StringList) ([]specs.Platform, error) {
+	var plats []specs.Platform
+	if platform, ok := project.Environment["DOCKER_DEFAULT_PLATFORM"]; ok {
+		if !utils.StringContains(platformList, platform) {
+			return nil, fmt.Errorf("the DOCKER_DEFAULT_PLATFORM value should be part of the service.build.platforms: %q", platform)
+		}
+		p, err := platforms.Parse(platform)
+		if err != nil {
+			return nil, err
+		}
+		plats = append(plats, p)
+	}
+	return plats, nil
+}

+ 204 - 23
pkg/compose/build_buildkit.go

@@ -18,29 +18,36 @@ package compose
 
 import (
 	"context"
+	"fmt"
+	"net/url"
 	"os"
 	"path/filepath"
+	"strings"
+
+	ctxkube "github.com/docker/buildx/driver/kubernetes/context"
+	"github.com/docker/buildx/store"
+	"github.com/docker/buildx/store/storeutil"
+	"github.com/docker/cli/cli/command"
+	"github.com/docker/cli/cli/context/docker"
+	ctxstore "github.com/docker/cli/cli/context/store"
+	dockerclient "github.com/docker/docker/client"
+	"github.com/sirupsen/logrus"
+	"golang.org/x/sync/errgroup"
+	"k8s.io/client-go/tools/clientcmd"
 
-	"github.com/compose-spec/compose-go/types"
 	"github.com/docker/buildx/build"
 	"github.com/docker/buildx/driver"
+	_ "github.com/docker/buildx/driver/docker"           //nolint:blank-imports
+	_ "github.com/docker/buildx/driver/docker-container" //nolint:blank-imports
+	_ "github.com/docker/buildx/driver/kubernetes"       //nolint:blank-imports
 	xprogress "github.com/docker/buildx/util/progress"
-
-	"github.com/docker/compose/v2/pkg/api"
 )
 
-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
@@ -49,17 +56,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
 	defer cancel()
 	w := xprogress.NewPrinter(progressCtx, s.stdout(), os.Stdout, mode)
 
-	for k := range opts {
-		if opts[k].Labels == nil {
-			opt := opts[k]
-			opt.Labels = make(map[string]string)
-			opts[k] = opt
-		}
-		opts[k].Labels[api.ImageBuilderLabel] = "buildkit"
-	}
-
-	// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
-	response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile().Filename), w)
+	response, err := build.Build(ctx, dis, opts, &internalAPI{dockerCli: s.dockerCli}, filepath.Dir(s.configFile().Filename), w)
 	errW := w.Wait()
 	if err == nil {
 		err = errW
@@ -82,3 +79,187 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
 
 	return imagesBuilt, err
 }
+
+func (s *composeService) getDrivers(ctx context.Context) ([]build.DriverInfo, error) { //nolint:gocyclo
+	txn, release, err := storeutil.GetStore(s.dockerCli)
+	if err != nil {
+		return nil, err
+	}
+	defer release()
+
+	ng, err := storeutil.GetCurrentInstance(txn, s.dockerCli)
+	if err != nil {
+		return nil, err
+	}
+
+	dis := make([]build.DriverInfo, len(ng.Nodes))
+	var f driver.Factory
+	if ng.Driver != "" {
+		factories := driver.GetFactories()
+		for _, fac := range factories {
+			if fac.Name() == ng.Driver {
+				f = fac
+				continue
+			}
+		}
+		if f == nil {
+			if f = driver.GetFactory(ng.Driver, true); f == nil {
+				return nil, fmt.Errorf("failed to find buildx driver %q", ng.Driver)
+			}
+		}
+	} else {
+		ep := ng.Nodes[0].Endpoint
+		dockerapi, err := clientForEndpoint(s.dockerCli, ep)
+		if err != nil {
+			return nil, err
+		}
+		f, err = driver.GetDefaultFactory(ctx, dockerapi, false)
+		if err != nil {
+			return nil, err
+		}
+		ng.Driver = f.Name()
+	}
+
+	imageopt, err := storeutil.GetImageConfig(s.dockerCli, ng)
+	if err != nil {
+		return nil, err
+	}
+
+	eg, _ := errgroup.WithContext(ctx)
+	for i, n := range ng.Nodes {
+		func(i int, n store.Node) {
+			eg.Go(func() error {
+				di := build.DriverInfo{
+					Name:        n.Name,
+					Platform:    n.Platforms,
+					ProxyConfig: storeutil.GetProxyConfig(s.dockerCli),
+				}
+				defer func() {
+					dis[i] = di
+				}()
+
+				dockerapi, err := clientForEndpoint(s.dockerCli, n.Endpoint)
+				if err != nil {
+					di.Err = err
+					return nil
+				}
+				// TODO: replace the following line with dockerclient.WithAPIVersionNegotiation option in clientForEndpoint
+				dockerapi.NegotiateAPIVersion(ctx)
+
+				contextStore := s.dockerCli.ContextStore()
+
+				var kcc driver.KubeClientConfig
+				kcc, err = configFromContext(n.Endpoint, contextStore)
+				if err != nil {
+					// err is returned if n.Endpoint is non-context name like "unix:///var/run/docker.sock".
+					// try again with name="default".
+					// FIXME: n should retain real context name.
+					kcc, err = configFromContext("default", contextStore)
+					if err != nil {
+						logrus.Error(err)
+					}
+				}
+
+				tryToUseKubeConfigInCluster := false
+				if kcc == nil {
+					tryToUseKubeConfigInCluster = true
+				} else {
+					if _, err := kcc.ClientConfig(); err != nil {
+						tryToUseKubeConfigInCluster = true
+					}
+				}
+				if tryToUseKubeConfigInCluster {
+					kccInCluster := driver.KubeClientConfigInCluster{}
+					if _, err := kccInCluster.ClientConfig(); err == nil {
+						logrus.Debug("using kube config in cluster")
+						kcc = kccInCluster
+					}
+				}
+
+				d, err := driver.GetDriver(ctx, "buildx_buildkit_"+n.Name, f, dockerapi, imageopt.Auth, kcc, n.Flags, n.Files, n.DriverOpts, n.Platforms, "")
+				if err != nil {
+					di.Err = err
+					return nil
+				}
+				di.Driver = d
+				di.ImageOpt = imageopt
+				return nil
+			})
+		}(i, n)
+	}
+
+	if err := eg.Wait(); err != nil {
+		return nil, err
+	}
+
+	return dis, nil
+}
+
+func clientForEndpoint(dockerCli command.Cli, name string) (dockerclient.APIClient, error) {
+	list, err := dockerCli.ContextStore().List()
+	if err != nil {
+		return nil, err
+	}
+	for _, l := range list {
+		if l.Name != name {
+			continue
+		}
+		dep, ok := l.Endpoints["docker"]
+		if !ok {
+			return nil, fmt.Errorf("context %q does not have a Docker endpoint", name)
+		}
+		epm, ok := dep.(docker.EndpointMeta)
+		if !ok {
+			return nil, fmt.Errorf("endpoint %q is not of type EndpointMeta, %T", dep, dep)
+		}
+		ep, err := docker.WithTLSData(dockerCli.ContextStore(), name, epm)
+		if err != nil {
+			return nil, err
+		}
+		clientOpts, err := ep.ClientOpts()
+		if err != nil {
+			return nil, err
+		}
+		return dockerclient.NewClientWithOpts(clientOpts...)
+	}
+
+	ep := docker.Endpoint{
+		EndpointMeta: docker.EndpointMeta{
+			Host: name,
+		},
+	}
+
+	clientOpts, err := ep.ClientOpts()
+	if err != nil {
+		return nil, err
+	}
+
+	return dockerclient.NewClientWithOpts(clientOpts...)
+}
+
+func configFromContext(endpointName string, s ctxstore.Reader) (clientcmd.ClientConfig, error) {
+	if strings.HasPrefix(endpointName, "kubernetes://") {
+		u, _ := url.Parse(endpointName)
+		if kubeconfig := u.Query().Get("kubeconfig"); kubeconfig != "" {
+			_ = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, kubeconfig)
+		}
+		rules := clientcmd.NewDefaultClientConfigLoadingRules()
+		apiConfig, err := rules.Load()
+		if err != nil {
+			return nil, err
+		}
+		return clientcmd.NewDefaultClientConfig(*apiConfig, &clientcmd.ConfigOverrides{}), nil
+	}
+	return ctxkube.ConfigFromContext(endpointName, s)
+}
+
+type internalAPI struct {
+	dockerCli command.Cli
+}
+
+func (a *internalAPI) DockerAPI(name string) (dockerclient.APIClient, error) {
+	if name == "" {
+		name = a.dockerCli.CurrentContext()
+	}
+	return clientForEndpoint(a.dockerCli, name)
+}

+ 4 - 0
pkg/compose/build_classic.go

@@ -89,6 +89,10 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
 		}
 	}
 
+	if len(options.Platforms) > 1 {
+		return "", errors.Errorf("this builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use multi-arch builder")
+	}
+
 	if options.Labels == nil {
 		options.Labels = make(map[string]string)
 	}

+ 2 - 17
pkg/compose/compose.go

@@ -17,7 +17,6 @@
 package compose
 
 import (
-	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -96,28 +95,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{

+ 21 - 8
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) {

+ 17 - 10
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

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

+ 15 - 5
pkg/compose/down_test.go

@@ -40,7 +40,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(
@@ -88,7 +90,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(
@@ -125,7 +129,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(
@@ -166,7 +172,9 @@ func TestDownRemoveImages(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)).
@@ -255,7 +263,9 @@ func TestDownRemoveImages_NoLabel(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)

+ 11 - 5
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),
 	}
 }

+ 20 - 1
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

+ 204 - 0
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]
+}

+ 3 - 1
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()

+ 12 - 0
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,

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

+ 3 - 1
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()

+ 102 - 0
pkg/e2e/build_test.go

@@ -257,3 +257,105 @@ func TestBuildImageDependencies(t *testing.T) {
 		t.Skip("See https://github.com/docker/compose/issues/9232")
 	})
 }
+
+func TestBuildPlatformsWithCorrectBuildxConfig(t *testing.T) {
+	c := NewParallelCLI(t)
+
+	// declare builder
+	result := c.RunDockerCmd(t, "buildx", "create", "--name", "build-platform", "--use", "--bootstrap")
+	assert.NilError(t, result.Error)
+
+	t.Cleanup(func() {
+		c.RunDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "down")
+		_ = c.RunDockerCmd(t, "buildx", "rm", "-f", "build-platform")
+	})
+
+	t.Run("platform not supported by builder", func(t *testing.T) {
+		res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
+			"-f", "fixtures/build-test/platforms/compose-unsupported-platform.yml", "build")
+		res.Assert(t, icmd.Expected{
+			ExitCode: 17,
+			Err:      "failed to solve: alpine: no match for platform in",
+		})
+	})
+
+	t.Run("multi-arch build ok", func(t *testing.T) {
+		res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
+		assert.NilError(t, res.Error, res.Stderr())
+		res.Assert(t, icmd.Expected{Out: "I am building for linux/arm64"})
+		res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
+
+	})
+
+	t.Run("multi-arch multi service builds ok", func(t *testing.T) {
+		res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
+			"-f", "fixtures/build-test/platforms/compose-multiple-platform-builds.yaml", "build")
+		assert.NilError(t, res.Error, res.Stderr())
+		res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/arm64"})
+		res.Assert(t, icmd.Expected{Out: "I'm Service A and I am building for linux/amd64"})
+		res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/arm64"})
+		res.Assert(t, icmd.Expected{Out: "I'm Service B and I am building for linux/amd64"})
+		res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/arm64"})
+		res.Assert(t, icmd.Expected{Out: "I'm Service C and I am building for linux/amd64"})
+	})
+
+	t.Run("multi-arch up --build", func(t *testing.T) {
+		res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build")
+		assert.NilError(t, res.Error, res.Stderr())
+		res.Assert(t, icmd.Expected{Out: "platforms-platforms-1 exited with code 0"})
+	})
+
+	t.Run("use DOCKER_DEFAULT_PLATFORM value when up --build", func(t *testing.T) {
+		cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "up", "--build")
+		res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
+			cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=linux/amd64")
+		})
+		assert.NilError(t, res.Error, res.Stderr())
+		res.Assert(t, icmd.Expected{Out: "I am building for linux/amd64"})
+		assert.Assert(t, !strings.Contains(res.Stdout(), "I am building for linux/arm64"))
+	})
+}
+
+func TestBuildPlatformsStandardErrors(t *testing.T) {
+	c := NewParallelCLI(t)
+
+	t.Run("no platform support with Classic Builder", func(t *testing.T) {
+		cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build")
+
+		res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
+			cmd.Env = append(cmd.Env, "DOCKER_BUILDKIT=0")
+		})
+		res.Assert(t, icmd.Expected{
+			ExitCode: 1,
+			Err:      "this builder doesn't support multi-arch build, set DOCKER_BUILDKIT=1 to use multi-arch builder",
+		})
+	})
+
+	t.Run("builder does not support multi-arch", func(t *testing.T) {
+		res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms", "build")
+		res.Assert(t, icmd.Expected{
+			ExitCode: 17,
+			Err:      `multiple platforms feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")`,
+		})
+	})
+
+	t.Run("service platform not defined in platforms build section", func(t *testing.T) {
+		res := c.RunDockerComposeCmdNoCheck(t, "--project-directory", "fixtures/build-test/platforms",
+			"-f", "fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml", "build")
+		res.Assert(t, icmd.Expected{
+			ExitCode: 1,
+			Err:      `service.platform should be part of the service.build.platforms: "linux/riscv64"`,
+		})
+	})
+
+	t.Run("DOCKER_DEFAULT_PLATFORM value not defined in platforms build section", func(t *testing.T) {
+		cmd := c.NewDockerComposeCmd(t, "--project-directory", "fixtures/build-test/platforms", "build")
+		res := icmd.RunCmd(cmd, func(cmd *icmd.Cmd) {
+			cmd.Env = append(cmd.Env, "DOCKER_DEFAULT_PLATFORM=windows/amd64")
+		})
+		res.Assert(t, icmd.Expected{
+			ExitCode: 1,
+			Err:      `DOCKER_DEFAULT_PLATFORM value should be part of the service.build.platforms: "windows/amd64"`,
+		})
+	})
+}

+ 22 - 0
pkg/e2e/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})
+	})
+}

+ 22 - 0
pkg/e2e/fixtures/build-test/platforms/Dockerfile

@@ -0,0 +1,22 @@
+#   Copyright 2020 Docker Compose CLI authors
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+FROM --platform=$BUILDPLATFORM golang:alpine AS build
+
+ARG TARGETPLATFORM
+ARG BUILDPLATFORM
+RUN echo "I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

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

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

+ 9 - 0
pkg/e2e/fixtures/build-test/platforms/compose-service-platform-not-in-build-platforms.yaml

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

+ 8 - 0
pkg/e2e/fixtures/build-test/platforms/compose-unsupported-platform.yml

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

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

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

+ 22 - 0
pkg/e2e/fixtures/build-test/platforms/contextServiceA/Dockerfile

@@ -0,0 +1,22 @@
+#   Copyright 2020 Docker Compose CLI authors
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+FROM --platform=$BUILDPLATFORM golang:alpine AS build
+
+ARG TARGETPLATFORM
+ARG BUILDPLATFORM
+RUN echo "I'm Service A and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 22 - 0
pkg/e2e/fixtures/build-test/platforms/contextServiceB/Dockerfile

@@ -0,0 +1,22 @@
+#   Copyright 2020 Docker Compose CLI authors
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+FROM --platform=$BUILDPLATFORM golang:alpine AS build
+
+ARG TARGETPLATFORM
+ARG BUILDPLATFORM
+RUN echo "I'm Service B and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 22 - 0
pkg/e2e/fixtures/build-test/platforms/contextServiceC/Dockerfile

@@ -0,0 +1,22 @@
+#   Copyright 2020 Docker Compose CLI authors
+
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+
+#       http://www.apache.org/licenses/LICENSE-2.0
+
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+FROM --platform=$BUILDPLATFORM golang:alpine AS build
+
+ARG TARGETPLATFORM
+ARG BUILDPLATFORM
+RUN echo "I'm Service C and I am building for $TARGETPLATFORM, running on $BUILDPLATFORM" > /log
+
+FROM alpine
+COPY --from=build /log /log

+ 5 - 0
pkg/e2e/fixtures/simple-build-test/compose-interpolate.yaml

@@ -0,0 +1,5 @@
+services:
+  nginx:
+    build:
+      context: nginx-build
+      dockerfile: ${MYVAR}

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

+ 24 - 0
pkg/e2e/start_stop_test.go

@@ -247,6 +247,30 @@ func TestStartStopMultipleServices(t *testing.T) {
 	}
 }
 
+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) {
 	cli := NewParallelCLI(t, WithEnv("COMPOSE_PROJECT_NAME=e2e-start-stop-svc-multiple-files"))
 	t.Cleanup(func() {

+ 2 - 0
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.

+ 4 - 1
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)
 	}
 

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

+ 30 - 0
pkg/utils/slices.go

@@ -0,0 +1,30 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package utils
+
+import "reflect"
+
+// Contains helps to detect if a non-comparable struct is part of an array
+// only use this method if you can't rely on existing golang Contains function of slices (https://pkg.go.dev/golang.org/x/exp/slices#Contains)
+func Contains[T any](origin []T, element T) bool {
+	for _, v := range origin {
+		if reflect.DeepEqual(v, element) {
+			return true
+		}
+	}
+	return false
+}

+ 95 - 0
pkg/utils/slices_test.go

@@ -0,0 +1,95 @@
+/*
+   Copyright 2020 Docker Compose CLI authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package utils
+
+import (
+	"testing"
+
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
+)
+
+func TestContains(t *testing.T) {
+	source := []specs.Platform{
+		{
+			Architecture: "linux/amd64",
+			OS:           "darwin",
+			OSVersion:    "",
+			OSFeatures:   nil,
+			Variant:      "",
+		},
+		{
+			Architecture: "linux/arm64",
+			OS:           "linux",
+			OSVersion:    "12",
+			OSFeatures:   nil,
+			Variant:      "v8",
+		},
+		{
+			Architecture: "",
+			OS:           "",
+			OSVersion:    "",
+			OSFeatures:   nil,
+			Variant:      "",
+		},
+	}
+
+	type args struct {
+		origin  []specs.Platform
+		element specs.Platform
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "element found",
+			args: args{
+				origin: source,
+				element: specs.Platform{
+					Architecture: "linux/arm64",
+					OS:           "linux",
+					OSVersion:    "12",
+					OSFeatures:   nil,
+					Variant:      "v8",
+				},
+			},
+			want: true,
+		},
+		{
+			name: "element not found",
+			args: args{
+				origin: source,
+				element: specs.Platform{
+					Architecture: "linux/arm64",
+					OS:           "darwin",
+					OSVersion:    "12",
+					OSFeatures:   nil,
+					Variant:      "v8",
+				},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := Contains(tt.args.origin, tt.args.element); got != tt.want {
+				t.Errorf("Contains() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}