Forráskód Böngészése

Merge pull request #1539 from docker/inject

Nicolas De loof 4 éve
szülő
commit
fa05d4397a
45 módosított fájl, 951 hozzáadás és 449 törlés
  1. 2 2
      Dockerfile
  2. 143 0
      api/compose/delegator.go
  3. 143 0
      api/compose/noimpl.go
  4. 42 11
      builder.Makefile
  5. 6 12
      cli/cmd/compose/build.go
  6. 79 26
      cli/cmd/compose/compose.go
  7. 7 17
      cli/cmd/compose/convert.go
  8. 7 4
      cli/cmd/compose/create.go
  9. 9 13
      cli/cmd/compose/down.go
  10. 6 12
      cli/cmd/compose/events.go
  11. 6 12
      cli/cmd/compose/exec.go
  12. 6 12
      cli/cmd/compose/images.go
  13. 6 11
      cli/cmd/compose/kill.go
  14. 6 11
      cli/cmd/compose/list.go
  15. 6 12
      cli/cmd/compose/logs.go
  16. 12 23
      cli/cmd/compose/pause.go
  17. 6 12
      cli/cmd/compose/port.go
  18. 6 12
      cli/cmd/compose/ps.go
  19. 7 13
      cli/cmd/compose/pull.go
  20. 6 12
      cli/cmd/compose/push.go
  21. 8 14
      cli/cmd/compose/remove.go
  22. 6 12
      cli/cmd/compose/restart.go
  23. 11 12
      cli/cmd/compose/run.go
  24. 6 12
      cli/cmd/compose/start.go
  25. 7 11
      cli/cmd/compose/stop.go
  26. 7 11
      cli/cmd/compose/top.go
  27. 20 24
      cli/cmd/compose/up.go
  28. 35 0
      cli/config/flags.go
  29. 61 0
      cli/config/flags_test.go
  30. 5 59
      cli/main.go
  31. 0 36
      cli/main_test.go
  32. 23 0
      cli/metrics/definitions.go
  33. 2 6
      cli/mobycli/exec.go
  34. 1 1
      docs/yaml/main/generate.go
  35. 4 2
      ecs/local/backend.go
  36. 33 1
      local/backend.go
  37. 1 8
      local/compose/build.go
  38. 6 3
      local/compose/compose.go
  39. 1 7
      local/compose/pull.go
  40. 21 0
      local/e2e/compose/compose_test.go
  41. 3 0
      local/e2e/compose/fixtures/build-infinite/docker-compose.yml
  42. 17 0
      local/e2e/compose/fixtures/build-infinite/service1/Dockerfile
  43. 70 0
      local/e2e/compose/metrics_test.go
  44. 66 0
      main.go
  45. 26 3
      utils/e2e/framework.go

+ 2 - 2
Dockerfile

@@ -73,7 +73,7 @@ RUN --mount=target=. \
     GOARCH=${TARGETARCH} \
     BUILD_TAGS=${BUILD_TAGS} \
     GIT_TAG=${GIT_TAG} \
-    make BINARY=/out/docker -f builder.Makefile cli
+    make BINARY=/out/docker COMPOSE_BINARY=/out/docker-compose -f builder.Makefile cli
 
 FROM base AS make-cross
 ARG BUILD_TAGS
@@ -83,7 +83,7 @@ RUN --mount=target=. \
     --mount=type=cache,target=/root/.cache/go-build \
     BUILD_TAGS=${BUILD_TAGS} \
     GIT_TAG=${GIT_TAG} \
-    make BINARY=/out/docker  -f builder.Makefile cross
+    make BINARY=/out/docker COMPOSE_BINARY=/out/docker-compose -f builder.Makefile cross
 
 FROM scratch AS protos
 COPY --from=make-protos /compose-cli/cli/server/protos .

+ 143 - 0
api/compose/delegator.go

@@ -0,0 +1,143 @@
+/*
+   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 (
+	"context"
+
+	"github.com/compose-spec/compose-go/types"
+)
+
+// ServiceDelegator implements Service by delegating to another implementation. This allows lazy init
+type ServiceDelegator struct {
+	Delegate Service
+}
+
+//Build implements Service interface
+func (s *ServiceDelegator) Build(ctx context.Context, project *types.Project, options BuildOptions) error {
+	return s.Delegate.Build(ctx, project, options)
+}
+
+//Push implements Service interface
+func (s *ServiceDelegator) Push(ctx context.Context, project *types.Project, options PushOptions) error {
+	return s.Delegate.Push(ctx, project, options)
+}
+
+//Pull implements Service interface
+func (s *ServiceDelegator) Pull(ctx context.Context, project *types.Project, options PullOptions) error {
+	return s.Delegate.Pull(ctx, project, options)
+}
+
+//Create implements Service interface
+func (s *ServiceDelegator) Create(ctx context.Context, project *types.Project, options CreateOptions) error {
+	return s.Delegate.Create(ctx, project, options)
+}
+
+//Start implements Service interface
+func (s *ServiceDelegator) Start(ctx context.Context, project *types.Project, options StartOptions) error {
+	return s.Delegate.Start(ctx, project, options)
+}
+
+//Restart implements Service interface
+func (s *ServiceDelegator) Restart(ctx context.Context, project *types.Project, options RestartOptions) error {
+	return s.Delegate.Restart(ctx, project, options)
+}
+
+//Stop implements Service interface
+func (s *ServiceDelegator) Stop(ctx context.Context, project *types.Project, options StopOptions) error {
+	return s.Delegate.Stop(ctx, project, options)
+}
+
+//Up implements Service interface
+func (s *ServiceDelegator) Up(ctx context.Context, project *types.Project, options UpOptions) error {
+	return s.Delegate.Up(ctx, project, options)
+}
+
+//Down implements Service interface
+func (s *ServiceDelegator) Down(ctx context.Context, project string, options DownOptions) error {
+	return s.Delegate.Down(ctx, project, options)
+}
+
+//Logs implements Service interface
+func (s *ServiceDelegator) Logs(ctx context.Context, project string, consumer LogConsumer, options LogOptions) error {
+	return s.Delegate.Logs(ctx, project, consumer, options)
+}
+
+//Ps implements Service interface
+func (s *ServiceDelegator) Ps(ctx context.Context, project string, options PsOptions) ([]ContainerSummary, error) {
+	return s.Delegate.Ps(ctx, project, options)
+}
+
+//List implements Service interface
+func (s *ServiceDelegator) List(ctx context.Context, options ListOptions) ([]Stack, error) {
+	return s.Delegate.List(ctx, options)
+}
+
+//Convert implements Service interface
+func (s *ServiceDelegator) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) {
+	return s.Delegate.Convert(ctx, project, options)
+}
+
+//Kill implements Service interface
+func (s *ServiceDelegator) Kill(ctx context.Context, project *types.Project, options KillOptions) error {
+	return s.Delegate.Kill(ctx, project, options)
+}
+
+//RunOneOffContainer implements Service interface
+func (s *ServiceDelegator) RunOneOffContainer(ctx context.Context, project *types.Project, options RunOptions) (int, error) {
+	return s.Delegate.RunOneOffContainer(ctx, project, options)
+}
+
+//Remove implements Service interface
+func (s *ServiceDelegator) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) {
+	return s.Delegate.Remove(ctx, project, options)
+}
+
+//Exec implements Service interface
+func (s *ServiceDelegator) Exec(ctx context.Context, project *types.Project, options RunOptions) error {
+	return s.Delegate.Exec(ctx, project, options)
+}
+
+//Pause implements Service interface
+func (s *ServiceDelegator) Pause(ctx context.Context, project string, options PauseOptions) error {
+	return s.Delegate.Pause(ctx, project, options)
+}
+
+//UnPause implements Service interface
+func (s *ServiceDelegator) UnPause(ctx context.Context, project string, options PauseOptions) error {
+	return s.Delegate.UnPause(ctx, project, options)
+}
+
+//Top implements Service interface
+func (s *ServiceDelegator) Top(ctx context.Context, project string, services []string) ([]ContainerProcSummary, error) {
+	return s.Delegate.Top(ctx, project, services)
+}
+
+//Events implements Service interface
+func (s *ServiceDelegator) Events(ctx context.Context, project string, options EventsOptions) error {
+	return s.Delegate.Events(ctx, project, options)
+}
+
+//Port implements Service interface
+func (s *ServiceDelegator) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) {
+	return s.Delegate.Port(ctx, project, service, port, options)
+}
+
+//Images implements Service interface
+func (s *ServiceDelegator) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
+	return s.Delegate.Images(ctx, project, options)
+}

+ 143 - 0
api/compose/noimpl.go

@@ -0,0 +1,143 @@
+/*
+   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 (
+	"context"
+
+	"github.com/compose-spec/compose-go/types"
+
+	"github.com/docker/compose-cli/api/errdefs"
+)
+
+// NoImpl implements Service to return ErrNotImplemented
+type NoImpl struct{}
+
+//Build implements Service interface
+func (s NoImpl) Build(ctx context.Context, project *types.Project, options BuildOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Push implements Service interface
+func (s NoImpl) Push(ctx context.Context, project *types.Project, options PushOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Pull implements Service interface
+func (s NoImpl) Pull(ctx context.Context, project *types.Project, options PullOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Create implements Service interface
+func (s NoImpl) Create(ctx context.Context, project *types.Project, options CreateOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Start implements Service interface
+func (s NoImpl) Start(ctx context.Context, project *types.Project, options StartOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Restart implements Service interface
+func (s NoImpl) Restart(ctx context.Context, project *types.Project, options RestartOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Stop implements Service interface
+func (s NoImpl) Stop(ctx context.Context, project *types.Project, options StopOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Up implements Service interface
+func (s NoImpl) Up(ctx context.Context, project *types.Project, options UpOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Down implements Service interface
+func (s NoImpl) Down(ctx context.Context, project string, options DownOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Logs implements Service interface
+func (s NoImpl) Logs(ctx context.Context, project string, consumer LogConsumer, options LogOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Ps implements Service interface
+func (s NoImpl) Ps(ctx context.Context, project string, options PsOptions) ([]ContainerSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//List implements Service interface
+func (s NoImpl) List(ctx context.Context, options ListOptions) ([]Stack, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Convert implements Service interface
+func (s NoImpl) Convert(ctx context.Context, project *types.Project, options ConvertOptions) ([]byte, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Kill implements Service interface
+func (s NoImpl) Kill(ctx context.Context, project *types.Project, options KillOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//RunOneOffContainer implements Service interface
+func (s NoImpl) RunOneOffContainer(ctx context.Context, project *types.Project, options RunOptions) (int, error) {
+	return 0, errdefs.ErrNotImplemented
+}
+
+//Remove implements Service interface
+func (s NoImpl) Remove(ctx context.Context, project *types.Project, options RemoveOptions) ([]string, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Exec implements Service interface
+func (s NoImpl) Exec(ctx context.Context, project *types.Project, options RunOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Pause implements Service interface
+func (s NoImpl) Pause(ctx context.Context, project string, options PauseOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//UnPause implements Service interface
+func (s NoImpl) UnPause(ctx context.Context, project string, options PauseOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Top implements Service interface
+func (s NoImpl) Top(ctx context.Context, project string, services []string) ([]ContainerProcSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}
+
+//Events implements Service interface
+func (s NoImpl) Events(ctx context.Context, project string, options EventsOptions) error {
+	return errdefs.ErrNotImplemented
+}
+
+//Port implements Service interface
+func (s NoImpl) Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) {
+	return "", 0, errdefs.ErrNotImplemented
+}
+
+//Images implements Service interface
+func (s NoImpl) Images(ctx context.Context, project string, options ImagesOptions) ([]ImageSummary, error) {
+	return nil, errdefs.ErrNotImplemented
+}

+ 42 - 11
builder.Makefile

@@ -34,6 +34,9 @@ GO_BUILD=$(STATIC_FLAGS) go build -trimpath -ldflags=$(LDFLAGS)
 BINARY?=bin/docker
 BINARY_WITH_EXTENSION=$(BINARY)$(EXTENSION)
 
+COMPOSE_BINARY?=bin/docker-compose
+COMPOSE_BINARY_WITH_EXTENSION=$(COMPOSE_BINARY)$(EXTENSION)
+
 WORK_DIR:=$(shell mktemp -d)
 
 TAGS:=
@@ -42,9 +45,22 @@ ifdef BUILD_TAGS
   LINT_TAGS=--build-tags $(BUILD_TAGS)
 endif
 
-TAR_TRANSFORM:=--transform s/packaging/docker/ --transform s/bin/docker/ --transform s/docker-linux-amd64/docker/ --transform s/docker-darwin-amd64/docker/ --transform s/docker-linux-arm64/docker/ --transform s/docker-linux-armv6/docker/ --transform s/docker-linux-armv7/docker/ --transform s/docker-darwin-arm64/docker/
+TAR_TRANSFORM:=--transform s/packaging/docker/ --transform s/bin/docker/ \
+				--transform s/docker-linux-amd64/docker/ --transform s/docker-linux-arm64/docker/ \
+				--transform s/docker-linux-armv6/docker/ --transform s/docker-linux-armv7/docker/ \
+				--transform s/docker-darwin-amd64/docker/ --transform s/docker-darwin-arm64/docker/ \
+				--transform s/docker-compose-linux-amd64/docker-compose/ --transform s/docker-compose-linux-arm64/docker-compose/ \
+				--transform s/docker-compose-linux-armv6/docker-compose/ --transform s/docker-compose-linux-armv7/docker-compose/ \
+				--transform s/docker-compose-darwin-amd64/docker-compose/ --transform s/docker-compose-darwin-arm64/docker-compose/
+
 ifneq ($(findstring bsd,$(shell tar --version)),)
-  TAR_TRANSFORM=-s /packaging/docker/ -s /bin/docker/ -s /docker-linux-amd64/docker/ -s /docker-darwin-amd64/docker/ -s /docker-linux-arm64/docker/ -s /docker-linux-armv6/docker/ -s /docker-linux-armv7/docker/ -s /docker-darwin-arm64/docker/
+  TAR_TRANSFORM=-s /packaging/docker/ -s /bin/docker/ \
+  				-s /docker-linux-amd64/docker/  -s /docker-linux-arm64/docker/ \
+  				-s /docker-linux-armv6/docker/  -s /docker-linux-armv7/docker/ \
+				-s /docker-darwin-amd64/docker/	 -s /docker-darwin-arm64/docker/ \
+  				-s /docker-compose-linux-amd64/docker-compose/  -s /docker-compose-linux-arm64/docker-compose/ \
+  				-s /docker-compose-linux-armv6/docker-compose/  -s /docker-compose-linux-armv7/docker-compose/ \
+				-s /docker-compose-darwin-amd64/docker-compose/	 -s /docker-compose-darwin-arm64/docker-compose/
 endif
 
 all: cli
@@ -54,11 +70,15 @@ protos:
 	protoc -I. --go_out=plugins=grpc,paths=source_relative:. ${PROTOS}
 
 .PHONY: cli
-cli:
+cli: compose-plugin
 	GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(BINARY_WITH_EXTENSION) ./cli
 
+.PHONY: compose-plugin
+compose-plugin:
+	GOOS=${GOOS} GOARCH=${GOARCH} $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY_WITH_EXTENSION) .
+
 .PHONY: cross
-cross:
+cross: cross-compose-plugin
 	GOOS=linux   GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-amd64 ./cli
 	GOOS=linux   GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-arm64 ./cli
 	GOOS=linux   GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(BINARY)-linux-armv6 ./cli
@@ -67,6 +87,16 @@ cross:
 	GOOS=darwin  GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(BINARY)-darwin-arm64 ./cli
 	GOOS=windows GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(BINARY)-windows-amd64.exe ./cli
 
+.PHONY: cross-compose-plugin
+cross-compose-plugin:
+	GOOS=linux   GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-amd64 .
+	GOOS=linux   GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-arm64 .
+	GOOS=linux   GOARM=6 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv6 .
+	GOOS=linux   GOARM=7 GOARCH=arm $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-linux-armv7 .
+	GOOS=darwin  GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-amd64 .
+	GOOS=darwin  GOARCH=arm64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-darwin-arm64 .
+	GOOS=windows GOARCH=amd64 $(GO_BUILD) $(TAGS) -o $(COMPOSE_BINARY)-windows-amd64.exe .
+
 .PHONY: test
 test:
 	go test $(TAGS) -cover $(shell go list  $(TAGS) ./... | grep -vE 'e2e')
@@ -90,14 +120,15 @@ check-go-mod:
 .PHONY: package
 package: cross
 	mkdir -p dist
-	tar -czf dist/docker-linux-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-amd64
-	tar -czf dist/docker-linux-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-arm64
-	tar -czf dist/docker-linux-armv6.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv6
-	tar -czf dist/docker-linux-armv7.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv7
-	tar -czf dist/docker-darwin-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-amd64
-	tar -czf dist/docker-darwin-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-arm64
+	tar -czf dist/docker-linux-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-amd64 $(COMPOSE_BINARY)-linux-amd64
+	tar -czf dist/docker-linux-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-arm64 $(COMPOSE_BINARY)-linux-arm64
+	tar -czf dist/docker-linux-armv6.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv6 $(COMPOSE_BINARY)-linux-armv6
+	tar -czf dist/docker-linux-armv7.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-linux-armv7 $(COMPOSE_BINARY)-linux-armv7
+	tar -czf dist/docker-darwin-amd64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-amd64 $(COMPOSE_BINARY)-darwin-amd64
+	tar -czf dist/docker-darwin-arm64.tar.gz $(TAR_TRANSFORM) packaging/LICENSE $(BINARY)-darwin-arm64 $(COMPOSE_BINARY)-darwin-arm64
 	cp $(BINARY)-windows-amd64.exe $(WORK_DIR)/docker.exe
-	rm -f dist/docker-windows-amd64.zip && zip dist/docker-windows-amd64.zip -j packaging/LICENSE $(WORK_DIR)/docker.exe
+	cp $(COMPOSE_BINARY)-windows-amd64.exe $(WORK_DIR)/docker-compose.exe
+	rm -f dist/docker-windows-amd64.zip && zip dist/docker-windows-amd64.zip -j packaging/LICENSE $(WORK_DIR)/docker.exe $(WORK_DIR)/docker-compose.exe
 	rm -r $(WORK_DIR)
 
 .PHONY: yamldocs

+ 6 - 12
cli/cmd/compose/build.go

@@ -24,7 +24,6 @@ import (
 	"github.com/compose-spec/compose-go/types"
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
@@ -40,14 +39,14 @@ type buildOptions struct {
 	memory   string
 }
 
-func buildCommand(p *projectOptions) *cobra.Command {
+func buildCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := buildOptions{
 		projectOptions: p,
 	}
 	cmd := &cobra.Command{
 		Use:   "build [SERVICE...]",
 		Short: "Build or rebuild services",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if opts.memory != "" {
 				fmt.Println("WARNING --memory is ignored as not supported in buildkit.")
 			}
@@ -58,8 +57,8 @@ func buildCommand(p *projectOptions) *cobra.Command {
 				}
 				os.Stdout = devnull
 			}
-			return runBuild(cmd.Context(), opts, args)
-		},
+			return runBuild(ctx, backend, opts, args)
+		}),
 	}
 	cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "Don't print anything to STDOUT")
 	cmd.Flags().BoolVar(&opts.pull, "pull", false, "Always attempt to pull a newer version of the image.")
@@ -80,19 +79,14 @@ func buildCommand(p *projectOptions) *cobra.Command {
 	return cmd
 }
 
-func runBuild(ctx context.Context, opts buildOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runBuild(ctx context.Context, backend compose.Service, opts buildOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Build(ctx, project, compose.BuildOptions{
+		return "", backend.Build(ctx, project, compose.BuildOptions{
 			Pull:     opts.pull,
 			Progress: opts.progress,
 			Args:     types.NewMapping(opts.args),

+ 79 - 26
cli/cmd/compose/compose.go

@@ -17,22 +17,64 @@
 package compose
 
 import (
+	"context"
 	"fmt"
 	"os"
+	"os/signal"
 	"strings"
+	"syscall"
 
 	"github.com/compose-spec/compose-go/cli"
 	"github.com/compose-spec/compose-go/types"
+	dockercli "github.com/docker/cli/cli"
 	"github.com/morikuni/aec"
 	"github.com/pkg/errors"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
 
+	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/context/store"
+	"github.com/docker/compose-cli/api/errdefs"
 	"github.com/docker/compose-cli/cli/formatter"
 	"github.com/docker/compose-cli/cli/metrics"
 )
 
+//Command defines a compose CLI command as a func with args
+type Command func(context.Context, []string) error
+
+//Adapt a Command func to cobra library
+func Adapt(fn Command) func(cmd *cobra.Command, args []string) error {
+	return func(cmd *cobra.Command, args []string) error {
+		ctx := cmd.Context()
+		contextString := fmt.Sprintf("%s", ctx)
+		if !strings.HasSuffix(contextString, ".WithCancel") { // need to handle cancel
+			cancellableCtx, cancel := context.WithCancel(cmd.Context())
+			ctx = cancellableCtx
+			s := make(chan os.Signal, 1)
+			signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
+			go func() {
+				<-s
+				cancel()
+			}()
+		}
+		err := fn(ctx, args)
+		var composeErr metrics.ComposeError
+		if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
+			err = dockercli.StatusError{
+				StatusCode: 130,
+				Status:     metrics.CanceledStatus,
+			}
+		}
+		if errors.As(err, &composeErr) {
+			err = dockercli.StatusError{
+				StatusCode: composeErr.GetMetricsFailureCategory().ExitCode,
+				Status:     err.Error(),
+			}
+		}
+		return err
+	}
+}
+
 // Warning is a global warning to be displayed to user on command failure
 var Warning string
 
@@ -104,8 +146,8 @@ func (o *projectOptions) toProjectOptions(po ...cli.ProjectOptionsFn) (*cli.Proj
 			cli.WithName(o.ProjectName))...)
 }
 
-// Command returns the compose command with its child commands
-func Command(contextType string) *cobra.Command {
+// RootCommand returns the compose command with its child commands
+func RootCommand(contextType string, backend compose.Service) *cobra.Command {
 	opts := projectOptions{}
 	var ansi string
 	var noAnsi bool
@@ -119,9 +161,20 @@ func Command(contextType string) *cobra.Command {
 				return cmd.Help()
 			}
 			_ = cmd.Help()
-			return fmt.Errorf("unknown docker command: %q", "compose "+args[0])
+			return dockercli.StatusError{
+				StatusCode: metrics.CommandSyntaxFailure.ExitCode,
+				Status:     fmt.Sprintf("unknown docker command: %q", "compose "+args[0]),
+			}
 		},
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			parent := cmd.Root()
+			parentPrerun := parent.PersistentPreRunE
+			if parentPrerun != nil {
+				err := parentPrerun(cmd, args)
+				if err != nil {
+					return err
+				}
+			}
 			if noAnsi {
 				if ansi != "auto" {
 					return errors.New(`cannot specify DEPRECATED "--no-ansi" and "--ansi". Please use only "--ansi"`)
@@ -146,34 +199,34 @@ func Command(contextType string) *cobra.Command {
 	}
 
 	command.AddCommand(
-		upCommand(&opts, contextType),
-		downCommand(&opts, contextType),
-		startCommand(&opts),
-		restartCommand(&opts),
-		stopCommand(&opts),
-		psCommand(&opts),
-		listCommand(contextType),
-		logsCommand(&opts, contextType),
-		convertCommand(&opts),
-		killCommand(&opts),
-		runCommand(&opts),
-		removeCommand(&opts),
-		execCommand(&opts),
-		pauseCommand(&opts),
-		unpauseCommand(&opts),
-		topCommand(&opts),
-		eventsCommand(&opts),
-		portCommand(&opts),
-		imagesCommand(&opts),
+		upCommand(&opts, contextType, backend),
+		downCommand(&opts, contextType, backend),
+		startCommand(&opts, backend),
+		restartCommand(&opts, backend),
+		stopCommand(&opts, backend),
+		psCommand(&opts, backend),
+		listCommand(contextType, backend),
+		logsCommand(&opts, contextType, backend),
+		convertCommand(&opts, backend),
+		killCommand(&opts, backend),
+		runCommand(&opts, backend),
+		removeCommand(&opts, backend),
+		execCommand(&opts, backend),
+		pauseCommand(&opts, backend),
+		unpauseCommand(&opts, backend),
+		topCommand(&opts, backend),
+		eventsCommand(&opts, backend),
+		portCommand(&opts, backend),
+		imagesCommand(&opts, backend),
 		versionCommand(),
 	)
 
 	if contextType == store.LocalContextType || contextType == store.DefaultContextType {
 		command.AddCommand(
-			buildCommand(&opts),
-			pushCommand(&opts),
-			pullCommand(&opts),
-			createCommand(&opts),
+			buildCommand(&opts, backend),
+			pushCommand(&opts, backend),
+			pullCommand(&opts, backend),
+			createCommand(&opts, backend),
 		)
 	}
 	command.Flags().SetInterspersed(false)

+ 7 - 17
cli/cmd/compose/convert.go

@@ -31,9 +31,7 @@ import (
 	"github.com/opencontainers/go-digest"
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
-	"github.com/docker/compose-cli/api/config"
 	"github.com/docker/compose-cli/utils"
 )
 
@@ -52,7 +50,7 @@ type convertOptions struct {
 
 var addFlagsFuncs []func(cmd *cobra.Command, opts *convertOptions)
 
-func convertCommand(p *projectOptions) *cobra.Command {
+func convertCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := convertOptions{
 		projectOptions: p,
 	}
@@ -60,7 +58,7 @@ func convertCommand(p *projectOptions) *cobra.Command {
 		Aliases: []string{"config"},
 		Use:     "convert SERVICES",
 		Short:   "Converts the compose file to platform's canonical format",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if opts.quiet {
 				devnull, err := os.Open(os.DevNull)
 				if err != nil {
@@ -81,8 +79,8 @@ func convertCommand(p *projectOptions) *cobra.Command {
 				return runProfiles(opts, args)
 			}
 
-			return runConvert(cmd.Context(), opts, args)
-		},
+			return runConvert(ctx, backend, opts, args)
+		}),
 	}
 	flags := cmd.Flags()
 	flags.StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]")
@@ -102,23 +100,15 @@ func convertCommand(p *projectOptions) *cobra.Command {
 	return cmd
 }
 
-func runConvert(ctx context.Context, opts convertOptions, services []string) error {
+func runConvert(ctx context.Context, backend compose.Service, opts convertOptions, services []string) error {
 	var json []byte
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
 	project, err := opts.toProject(services, cli.WithInterpolation(!opts.noInterpolate))
 	if err != nil {
 		return err
 	}
 
 	if opts.resolve {
-		configFile, err := cliconfig.Load(config.Dir())
-		if err != nil {
-			return err
-		}
+		configFile := cliconfig.LoadDefaultConfigFile(os.Stderr)
 
 		resolver := remotes.CreateResolver(configFile)
 		err = project.ResolveImages(func(named reference.Named) (digest.Digest, error) {
@@ -130,7 +120,7 @@ func runConvert(ctx context.Context, opts convertOptions, services []string) err
 		}
 	}
 
-	json, err = c.ComposeService().Convert(ctx, project, compose.ConvertOptions{
+	json, err = backend.Convert(ctx, project, compose.ConvertOptions{
 		Format: opts.Format,
 		Output: opts.Output,
 	})

+ 7 - 4
cli/cmd/compose/create.go

@@ -17,9 +17,12 @@
 package compose
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/spf13/cobra"
+
+	"github.com/docker/compose-cli/api/compose"
 )
 
 type createOptions struct {
@@ -28,21 +31,21 @@ type createOptions struct {
 	noRecreate    bool
 }
 
-func createCommand(p *projectOptions) *cobra.Command {
+func createCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := createOptions{
 		composeOptions: &composeOptions{},
 	}
 	cmd := &cobra.Command{
 		Use:   "create [SERVICE...]",
 		Short: "Creates containers for a service.",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if opts.Build && opts.noBuild {
 				return fmt.Errorf("--build and --no-build are incompatible")
 			}
 			if opts.forceRecreate && opts.noRecreate {
 				return fmt.Errorf("--force-recreate and --no-recreate are incompatible")
 			}
-			return runCreateStart(cmd.Context(), upOptions{
+			return runCreateStart(ctx, backend, upOptions{
 				composeOptions: &composeOptions{
 					projectOptions: p,
 					Build:          opts.Build,
@@ -52,7 +55,7 @@ func createCommand(p *projectOptions) *cobra.Command {
 				forceRecreate: opts.forceRecreate,
 				noRecreate:    opts.noRecreate,
 			}, args)
-		},
+		}),
 	}
 	flags := cmd.Flags()
 	flags.BoolVar(&opts.Build, "build", false, "Build images before starting containers.")

+ 9 - 13
cli/cmd/compose/down.go

@@ -24,7 +24,6 @@ import (
 	"github.com/compose-spec/compose-go/types"
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/context/store"
 	"github.com/docker/compose-cli/api/progress"
@@ -39,22 +38,24 @@ type downOptions struct {
 	images        string
 }
 
-func downCommand(p *projectOptions, contextType string) *cobra.Command {
+func downCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command {
 	opts := downOptions{
 		projectOptions: p,
 	}
 	downCmd := &cobra.Command{
 		Use:   "down",
 		Short: "Stop and remove containers, networks",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		PreRun: func(cmd *cobra.Command, args []string) {
 			opts.timeChanged = cmd.Flags().Changed("timeout")
+		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if opts.images != "" {
 				if opts.images != "all" && opts.images != "local" {
 					return fmt.Errorf("invalid value for --rmi: %q", opts.images)
 				}
 			}
-			return runDown(cmd.Context(), opts)
-		},
+			return runDown(ctx, backend, opts)
+		}),
 	}
 	flags := downCmd.Flags()
 	flags.BoolVar(&opts.removeOrphans, "remove-orphans", false, "Remove containers for services not defined in the Compose file.")
@@ -68,13 +69,8 @@ func downCommand(p *projectOptions, contextType string) *cobra.Command {
 	return downCmd
 }
 
-func runDown(ctx context.Context, opts downOptions) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
-	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
+func runDown(ctx context.Context, backend compose.Service, opts downOptions) error {
+	_, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
 		name := opts.ProjectName
 		var project *types.Project
 		if opts.ProjectName == "" {
@@ -91,7 +87,7 @@ func runDown(ctx context.Context, opts downOptions) error {
 			timeoutValue := time.Duration(opts.timeout) * time.Second
 			timeout = &timeoutValue
 		}
-		return name, c.ComposeService().Down(ctx, name, compose.DownOptions{
+		return name, backend.Down(ctx, name, compose.DownOptions{
 			RemoveOrphans: opts.removeOrphans,
 			Project:       project,
 			Timeout:       timeout,

+ 6 - 12
cli/cmd/compose/events.go

@@ -21,7 +21,6 @@ import (
 	"encoding/json"
 	"fmt"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 
 	"github.com/spf13/cobra"
@@ -32,7 +31,7 @@ type eventsOpts struct {
 	json bool
 }
 
-func eventsCommand(p *projectOptions) *cobra.Command {
+func eventsCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := eventsOpts{
 		composeOptions: &composeOptions{
 			projectOptions: p,
@@ -41,27 +40,22 @@ func eventsCommand(p *projectOptions) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "events [options] [--] [SERVICE...]",
 		Short: "Receive real time events from containers.",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runEvents(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runEvents(ctx, backend, opts, args)
+		}),
 	}
 
 	cmd.Flags().BoolVar(&opts.json, "json", false, "Output events as a stream of json objects")
 	return cmd
 }
 
-func runEvents(ctx context.Context, opts eventsOpts, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runEvents(ctx context.Context, backend compose.Service, opts eventsOpts, services []string) error {
 	project, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
 
-	return c.ComposeService().Events(ctx, project, compose.EventsOptions{
+	return backend.Events(ctx, project, compose.EventsOptions{
 		Services: services,
 		Consumer: func(event compose.Event) error {
 			if opts.json {

+ 6 - 12
cli/cmd/compose/exec.go

@@ -24,7 +24,6 @@ import (
 	"github.com/containerd/console"
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 )
 
@@ -43,7 +42,7 @@ type execOpts struct {
 	privileged bool
 }
 
-func execCommand(p *projectOptions) *cobra.Command {
+func execCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := execOpts{
 		composeOptions: &composeOptions{
 			projectOptions: p,
@@ -53,13 +52,13 @@ func execCommand(p *projectOptions) *cobra.Command {
 		Use:   "exec [options] [-e KEY=VAL...] [--] SERVICE COMMAND [ARGS...]",
 		Short: "Execute a command in a running container.",
 		Args:  cobra.MinimumNArgs(2),
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if len(args) > 1 {
 				opts.command = args[1:]
 			}
 			opts.service = args[0]
-			return runExec(cmd.Context(), opts)
-		},
+			return runExec(ctx, backend, opts)
+		}),
 	}
 
 	runCmd.Flags().BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: Run command in the background.")
@@ -74,12 +73,7 @@ func execCommand(p *projectOptions) *cobra.Command {
 	return runCmd
 }
 
-func runExec(ctx context.Context, opts execOpts) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runExec(ctx context.Context, backend compose.Service, opts execOpts) error {
 	project, err := opts.toProject(nil)
 	if err != nil {
 		return err
@@ -114,5 +108,5 @@ func runExec(ctx context.Context, opts execOpts) error {
 		execOpts.Writer = con
 		execOpts.Reader = con
 	}
-	return c.ComposeService().Exec(ctx, project, execOpts)
+	return backend.Exec(ctx, project, execOpts)
 }

+ 6 - 12
cli/cmd/compose/images.go

@@ -26,7 +26,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/cli/formatter"
 	"github.com/docker/compose-cli/utils"
@@ -40,33 +39,28 @@ type imageOptions struct {
 	Quiet bool
 }
 
-func imagesCommand(p *projectOptions) *cobra.Command {
+func imagesCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := imageOptions{
 		projectOptions: p,
 	}
 	imgCmd := &cobra.Command{
 		Use:   "images [SERVICE...]",
 		Short: "List images used by the created containers",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runImages(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runImages(ctx, backend, opts, args)
+		}),
 	}
 	imgCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
 	return imgCmd
 }
 
-func runImages(ctx context.Context, opts imageOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runImages(ctx context.Context, backend compose.Service, opts imageOptions, services []string) error {
 	projectName, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
 
-	images, err := c.ComposeService().Images(ctx, projectName, compose.ImagesOptions{
+	images, err := backend.Images(ctx, projectName, compose.ImagesOptions{
 		Services: services,
 	})
 	if err != nil {

+ 6 - 11
cli/cmd/compose/kill.go

@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 )
 
@@ -30,16 +29,16 @@ type killOptions struct {
 	Signal string
 }
 
-func killCommand(p *projectOptions) *cobra.Command {
+func killCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := killOptions{
 		projectOptions: p,
 	}
 	cmd := &cobra.Command{
 		Use:   "kill [options] [SERVICE...]",
 		Short: "Force stop service containers.",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runKill(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runKill(ctx, backend, opts, args)
+		}),
 	}
 
 	flags := cmd.Flags()
@@ -48,16 +47,12 @@ func killCommand(p *projectOptions) *cobra.Command {
 	return cmd
 }
 
-func runKill(ctx context.Context, opts killOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
+func runKill(ctx context.Context, backend compose.Service, opts killOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
 	}
-	return c.ComposeService().Kill(ctx, project, compose.KillOptions{
+	return backend.Kill(ctx, project, compose.KillOptions{
 		Signal: opts.Signal,
 	})
 }

+ 6 - 11
cli/cmd/compose/list.go

@@ -26,7 +26,6 @@ import (
 	"github.com/docker/cli/opts"
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/context/store"
 	"github.com/docker/compose-cli/cli/formatter"
@@ -39,14 +38,14 @@ type lsOptions struct {
 	Filter opts.FilterOpt
 }
 
-func listCommand(contextType string) *cobra.Command {
+func listCommand(contextType string, backend compose.Service) *cobra.Command {
 	opts := lsOptions{Filter: opts.NewFilterOpt()}
 	lsCmd := &cobra.Command{
 		Use:   "ls",
 		Short: "List running compose projects",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runList(cmd.Context(), opts)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runList(ctx, backend, opts)
+		}),
 	}
 	lsCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].")
 	lsCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs.")
@@ -62,18 +61,14 @@ var acceptedListFilters = map[string]bool{
 	"name": true,
 }
 
-func runList(ctx context.Context, opts lsOptions) error {
+func runList(ctx context.Context, backend compose.Service, opts lsOptions) error {
 	filters := opts.Filter.Value()
 	err := filters.Validate(acceptedListFilters)
 	if err != nil {
 		return err
 	}
 
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-	stackList, err := c.ComposeService().List(ctx, compose.ListOptions{All: opts.All})
+	stackList, err := backend.List(ctx, compose.ListOptions{All: opts.All})
 	if err != nil {
 		return err
 	}

+ 6 - 12
cli/cmd/compose/logs.go

@@ -22,7 +22,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/context/store"
 	"github.com/docker/compose-cli/cli/formatter"
@@ -38,16 +37,16 @@ type logsOptions struct {
 	timestamps bool
 }
 
-func logsCommand(p *projectOptions, contextType string) *cobra.Command {
+func logsCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command {
 	opts := logsOptions{
 		projectOptions: p,
 	}
 	logsCmd := &cobra.Command{
 		Use:   "logs [service...]",
 		Short: "View output from containers",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runLogs(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runLogs(ctx, backend, opts, args)
+		}),
 	}
 	flags := logsCmd.Flags()
 	flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output.")
@@ -61,18 +60,13 @@ func logsCommand(p *projectOptions, contextType string) *cobra.Command {
 	return logsCmd
 }
 
-func runLogs(ctx context.Context, opts logsOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runLogs(ctx context.Context, backend compose.Service, opts logsOptions, services []string) error {
 	projectName, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
 	consumer := formatter.NewLogConsumer(ctx, os.Stdout, !opts.noColor, !opts.noPrefix)
-	return c.ComposeService().Logs(ctx, projectName, consumer, compose.LogOptions{
+	return backend.Logs(ctx, projectName, consumer, compose.LogOptions{
 		Services:   services,
 		Follow:     opts.follow,
 		Tail:       opts.tail,

+ 12 - 23
cli/cmd/compose/pause.go

@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
@@ -30,33 +29,28 @@ type pauseOptions struct {
 	*projectOptions
 }
 
-func pauseCommand(p *projectOptions) *cobra.Command {
+func pauseCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := pauseOptions{
 		projectOptions: p,
 	}
 	cmd := &cobra.Command{
 		Use:   "pause [SERVICE...]",
 		Short: "pause services",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runPause(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runPause(ctx, backend, opts, args)
+		}),
 	}
 	return cmd
 }
 
-func runPause(ctx context.Context, opts pauseOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runPause(ctx context.Context, backend compose.Service, opts pauseOptions, services []string) error {
 	project, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Pause(ctx, project, compose.PauseOptions{
+		return "", backend.Pause(ctx, project, compose.PauseOptions{
 			Services: services,
 		})
 	})
@@ -67,33 +61,28 @@ type unpauseOptions struct {
 	*projectOptions
 }
 
-func unpauseCommand(p *projectOptions) *cobra.Command {
+func unpauseCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := unpauseOptions{
 		projectOptions: p,
 	}
 	cmd := &cobra.Command{
 		Use:   "unpause [SERVICE...]",
 		Short: "unpause services",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runUnPause(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runUnPause(ctx, backend, opts, args)
+		}),
 	}
 	return cmd
 }
 
-func runUnPause(ctx context.Context, opts unpauseOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runUnPause(ctx context.Context, backend compose.Service, opts unpauseOptions, services []string) error {
 	project, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().UnPause(ctx, project, compose.PauseOptions{
+		return "", backend.UnPause(ctx, project, compose.PauseOptions{
 			Services: services,
 		})
 	})

+ 6 - 12
cli/cmd/compose/port.go

@@ -23,7 +23,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 )
 
@@ -33,7 +32,7 @@ type portOptions struct {
 	index    int
 }
 
-func portCommand(p *projectOptions) *cobra.Command {
+func portCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := portOptions{
 		projectOptions: p,
 	}
@@ -41,30 +40,25 @@ func portCommand(p *projectOptions) *cobra.Command {
 		Use:   "port [options] [--] SERVICE PRIVATE_PORT",
 		Short: "Print the public port for a port binding.",
 		Args:  cobra.MinimumNArgs(2),
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			port, err := strconv.Atoi(args[1])
 			if err != nil {
 				return err
 			}
-			return runPort(cmd.Context(), opts, args[0], port)
-		},
+			return runPort(ctx, backend, opts, args[0], port)
+		}),
 	}
 	cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp")
 	cmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if service has multiple replicas")
 	return cmd
 }
 
-func runPort(ctx context.Context, opts portOptions, service string, port int) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runPort(ctx context.Context, backend compose.Service, opts portOptions, service string, port int) error {
 	projectName, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
-	ip, port, err := c.ComposeService().Port(ctx, projectName, service, port, compose.PortOptions{
+	ip, port, err := backend.Port(ctx, projectName, service, port, compose.PortOptions{
 		Protocol: opts.protocol,
 		Index:    opts.index,
 	})

+ 6 - 12
cli/cmd/compose/ps.go

@@ -26,7 +26,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/cli/formatter"
 	"github.com/docker/compose-cli/utils"
@@ -40,16 +39,16 @@ type psOptions struct {
 	Services bool
 }
 
-func psCommand(p *projectOptions) *cobra.Command {
+func psCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := psOptions{
 		projectOptions: p,
 	}
 	psCmd := &cobra.Command{
 		Use:   "ps",
 		Short: "List containers",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runPs(cmd.Context(), opts)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runPs(ctx, backend, opts)
+		}),
 	}
 	psCmd.Flags().StringVar(&opts.Format, "format", "pretty", "Format the output. Values: [pretty | json].")
 	psCmd.Flags().BoolVarP(&opts.Quiet, "quiet", "q", false, "Only display IDs")
@@ -58,17 +57,12 @@ func psCommand(p *projectOptions) *cobra.Command {
 	return psCmd
 }
 
-func runPs(ctx context.Context, opts psOptions) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runPs(ctx context.Context, backend compose.Service, opts psOptions) error {
 	projectName, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
-	containers, err := c.ComposeService().Ps(ctx, projectName, compose.PsOptions{
+	containers, err := backend.Ps(ctx, projectName, compose.PsOptions{
 		All: opts.All,
 	})
 	if err != nil {

+ 7 - 13
cli/cmd/compose/pull.go

@@ -24,7 +24,6 @@ import (
 	"github.com/morikuni/aec"
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 	"github.com/docker/compose-cli/utils"
@@ -40,19 +39,19 @@ type pullOptions struct {
 	ignorePullFailures bool
 }
 
-func pullCommand(p *projectOptions) *cobra.Command {
+func pullCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := pullOptions{
 		projectOptions: p,
 	}
 	cmd := &cobra.Command{
 		Use:   "pull [SERVICE...]",
 		Short: "Pull service images",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if opts.noParallel {
 				fmt.Fprint(os.Stderr, aec.Apply("option '--no-parallel' is DEPRECATED and will be ignored.\n", aec.RedF))
 			}
-			return runPull(cmd.Context(), opts, args)
-		},
+			return runPull(ctx, backend, opts, args)
+		}),
 	}
 	flags := cmd.Flags()
 	flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Pull without printing progress information")
@@ -65,12 +64,7 @@ func pullCommand(p *projectOptions) *cobra.Command {
 	return cmd
 }
 
-func runPull(ctx context.Context, opts pullOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runPull(ctx context.Context, backend compose.Service, opts pullOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
@@ -94,11 +88,11 @@ func runPull(ctx context.Context, opts pullOptions, services []string) error {
 	}
 
 	if opts.quiet {
-		return c.ComposeService().Pull(ctx, project, apiOpts)
+		return backend.Pull(ctx, project, apiOpts)
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Pull(ctx, project, apiOpts)
+		return "", backend.Pull(ctx, project, apiOpts)
 	})
 	return err
 }

+ 6 - 12
cli/cmd/compose/push.go

@@ -21,7 +21,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
@@ -33,35 +32,30 @@ type pushOptions struct {
 	Ignorefailures bool
 }
 
-func pushCommand(p *projectOptions) *cobra.Command {
+func pushCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := pushOptions{
 		projectOptions: p,
 	}
 	pushCmd := &cobra.Command{
 		Use:   "push [SERVICE...]",
 		Short: "Push service images",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runPush(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runPush(ctx, backend, opts, args)
+		}),
 	}
 	pushCmd.Flags().BoolVar(&opts.Ignorefailures, "ignore-push-failures", false, "Push what it can and ignores images with push failures")
 
 	return pushCmd
 }
 
-func runPush(ctx context.Context, opts pushOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runPush(ctx context.Context, backend compose.Service, opts pushOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Push(ctx, project, compose.PushOptions{
+		return "", backend.Push(ctx, project, compose.PushOptions{
 			IgnoreFailures: opts.Ignorefailures,
 		})
 	})

+ 8 - 14
cli/cmd/compose/remove.go

@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 	"github.com/docker/compose-cli/utils/prompt"
@@ -36,7 +35,7 @@ type removeOptions struct {
 	volumes bool
 }
 
-func removeCommand(p *projectOptions) *cobra.Command {
+func removeCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := removeOptions{
 		projectOptions: p,
 	}
@@ -49,9 +48,9 @@ By default, anonymous volumes attached to containers will not be removed. You
 can override this with -v. To list all volumes, use "docker volume ls".
 
 Any data which is not in a volume will be lost.`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runRemove(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runRemove(ctx, backend, opts, args)
+		}),
 	}
 	f := cmd.Flags()
 	f.BoolVarP(&opts.force, "force", "f", false, "Don't ask to confirm removal")
@@ -60,12 +59,7 @@ Any data which is not in a volume will be lost.`,
 	return cmd
 }
 
-func runRemove(ctx context.Context, opts removeOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runRemove(ctx context.Context, backend compose.Service, opts removeOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
@@ -73,7 +67,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error
 
 	if opts.stop {
 		_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-			err := c.ComposeService().Stop(ctx, project, compose.StopOptions{
+			err := backend.Stop(ctx, project, compose.StopOptions{
 				Services: services,
 			})
 			return "", err
@@ -83,7 +77,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error
 		}
 	}
 
-	reosurces, err := c.ComposeService().Remove(ctx, project, compose.RemoveOptions{
+	reosurces, err := backend.Remove(ctx, project, compose.RemoveOptions{
 		DryRun:   true,
 		Services: services,
 	})
@@ -109,7 +103,7 @@ func runRemove(ctx context.Context, opts removeOptions, services []string) error
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		_, err = c.ComposeService().Remove(ctx, project, compose.RemoveOptions{
+		_, err = backend.Remove(ctx, project, compose.RemoveOptions{
 			Volumes: opts.volumes,
 			Force:   opts.force,
 		})

+ 6 - 12
cli/cmd/compose/restart.go

@@ -22,7 +22,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
@@ -32,16 +31,16 @@ type restartOptions struct {
 	timeout int
 }
 
-func restartCommand(p *projectOptions) *cobra.Command {
+func restartCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := restartOptions{
 		projectOptions: p,
 	}
 	restartCmd := &cobra.Command{
 		Use:   "restart",
 		Short: "Restart containers",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runRestart(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runRestart(ctx, backend, opts, args)
+		}),
 	}
 	flags := restartCmd.Flags()
 	flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
@@ -49,12 +48,7 @@ func restartCommand(p *projectOptions) *cobra.Command {
 	return restartCmd
 }
 
-func runRestart(ctx context.Context, opts restartOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runRestart(ctx context.Context, backend compose.Service, opts restartOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
@@ -62,7 +56,7 @@ func runRestart(ctx context.Context, opts restartOptions, services []string) err
 
 	timeout := time.Duration(opts.timeout) * time.Second
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Restart(ctx, project, compose.RestartOptions{
+		return "", backend.Restart(ctx, project, compose.RestartOptions{
 			Timeout: &timeout,
 		})
 	})

+ 11 - 12
cli/cmd/compose/run.go

@@ -28,7 +28,6 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/docker/cli/cli"
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
@@ -100,7 +99,7 @@ func (opts runOptions) apply(project *types.Project) error {
 	return nil
 }
 
-func runCommand(p *projectOptions) *cobra.Command {
+func runCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := runOptions{
 		composeOptions: &composeOptions{
 			projectOptions: p,
@@ -110,7 +109,7 @@ func runCommand(p *projectOptions) *cobra.Command {
 		Use:   "run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...] SERVICE [COMMAND] [ARGS...]",
 		Short: "Run a one-off command on a service.",
 		Args:  cobra.MinimumNArgs(1),
-		RunE: func(cmd *cobra.Command, args []string) error {
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			if len(args) > 1 {
 				opts.Command = args[1:]
 			}
@@ -118,8 +117,8 @@ func runCommand(p *projectOptions) *cobra.Command {
 			if len(opts.publish) > 0 && opts.servicePorts {
 				return fmt.Errorf("--service-ports and --publish are incompatible")
 			}
-			return runRun(cmd.Context(), opts)
-		},
+			return runRun(ctx, backend, opts)
+		}),
 	}
 	flags := cmd.Flags()
 	flags.BoolVarP(&opts.Detach, "detach", "d", false, "Run container in background and print container ID")
@@ -141,8 +140,8 @@ func runCommand(p *projectOptions) *cobra.Command {
 	return cmd
 }
 
-func runRun(ctx context.Context, opts runOptions) error {
-	c, project, err := setup(ctx, *opts.composeOptions, []string{opts.Service})
+func runRun(ctx context.Context, backend compose.Service, opts runOptions) error {
+	project, err := setup(*opts.composeOptions, []string{opts.Service})
 	if err != nil {
 		return err
 	}
@@ -153,7 +152,7 @@ func runRun(ctx context.Context, opts runOptions) error {
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", startDependencies(ctx, c, *project, opts.Service)
+		return "", startDependencies(ctx, backend, *project, opts.Service)
 	})
 	if err != nil {
 		return err
@@ -194,7 +193,7 @@ func runRun(ctx context.Context, opts runOptions) error {
 		UseNetworkAliases: opts.useAliases,
 		Index:             0,
 	}
-	exitCode, err := c.ComposeService().RunOneOffContainer(ctx, project, runOpts)
+	exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
 	if exitCode != 0 {
 		errMsg := ""
 		if err != nil {
@@ -205,7 +204,7 @@ func runRun(ctx context.Context, opts runOptions) error {
 	return err
 }
 
-func startDependencies(ctx context.Context, c *client.Client, project types.Project, requestedServiceName string) error {
+func startDependencies(ctx context.Context, backend compose.Service, project types.Project, requestedServiceName string) error {
 	dependencies := types.Services{}
 	var requestedService types.ServiceConfig
 	for _, service := range project.Services {
@@ -218,10 +217,10 @@ func startDependencies(ctx context.Context, c *client.Client, project types.Proj
 
 	project.Services = dependencies
 	project.DisabledServices = append(project.DisabledServices, requestedService)
-	if err := c.ComposeService().Create(ctx, &project, compose.CreateOptions{}); err != nil {
+	if err := backend.Create(ctx, &project, compose.CreateOptions{}); err != nil {
 		return err
 	}
-	if err := c.ComposeService().Start(ctx, &project, compose.StartOptions{}); err != nil {
+	if err := backend.Start(ctx, &project, compose.StartOptions{}); err != nil {
 		return err
 	}
 	return nil

+ 6 - 12
cli/cmd/compose/start.go

@@ -19,7 +19,6 @@ package compose
 import (
 	"context"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 
@@ -30,33 +29,28 @@ type startOptions struct {
 	*projectOptions
 }
 
-func startCommand(p *projectOptions) *cobra.Command {
+func startCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := startOptions{
 		projectOptions: p,
 	}
 	startCmd := &cobra.Command{
 		Use:   "start [SERVICE...]",
 		Short: "Start services",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runStart(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runStart(ctx, backend, opts, args)
+		}),
 	}
 	return startCmd
 }
 
-func runStart(ctx context.Context, opts startOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runStart(ctx context.Context, backend compose.Service, opts startOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Start(ctx, project, compose.StartOptions{})
+		return "", backend.Start(ctx, project, compose.StartOptions{})
 	})
 	return err
 }

+ 7 - 11
cli/cmd/compose/stop.go

@@ -22,7 +22,6 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/progress"
 )
@@ -33,17 +32,19 @@ type stopOptions struct {
 	timeout     int
 }
 
-func stopCommand(p *projectOptions) *cobra.Command {
+func stopCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := stopOptions{
 		projectOptions: p,
 	}
 	cmd := &cobra.Command{
 		Use:   "stop [SERVICE...]",
 		Short: "Stop services",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		PreRun: func(cmd *cobra.Command, args []string) {
 			opts.timeChanged = cmd.Flags().Changed("timeout")
-			return runStop(cmd.Context(), opts, args)
 		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runStop(ctx, backend, opts, args)
+		}),
 	}
 	flags := cmd.Flags()
 	flags.IntVarP(&opts.timeout, "timeout", "t", 10, "Specify a shutdown timeout in seconds")
@@ -51,12 +52,7 @@ func stopCommand(p *projectOptions) *cobra.Command {
 	return cmd
 }
 
-func runStop(ctx context.Context, opts stopOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
-
+func runStop(ctx context.Context, backend compose.Service, opts stopOptions, services []string) error {
 	project, err := opts.toProject(services)
 	if err != nil {
 		return err
@@ -68,7 +64,7 @@ func runStop(ctx context.Context, opts stopOptions, services []string) error {
 		timeout = &timeoutValue
 	}
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{
+		return "", backend.Stop(ctx, project, compose.StopOptions{
 			Timeout:  timeout,
 			Services: services,
 		})

+ 7 - 11
cli/cmd/compose/top.go

@@ -27,37 +27,33 @@ import (
 
 	"github.com/spf13/cobra"
 
-	"github.com/docker/compose-cli/api/client"
+	"github.com/docker/compose-cli/api/compose"
 )
 
 type topOptions struct {
 	*projectOptions
 }
 
-func topCommand(p *projectOptions) *cobra.Command {
+func topCommand(p *projectOptions, backend compose.Service) *cobra.Command {
 	opts := topOptions{
 		projectOptions: p,
 	}
 	topCmd := &cobra.Command{
 		Use:   "top",
 		Short: "Display the running processes",
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runTop(cmd.Context(), opts, args)
-		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
+			return runTop(ctx, backend, opts, args)
+		}),
 	}
 	return topCmd
 }
 
-func runTop(ctx context.Context, opts topOptions, services []string) error {
-	c, err := client.New(ctx)
-	if err != nil {
-		return err
-	}
+func runTop(ctx context.Context, backend compose.Service, opts topOptions, services []string) error {
 	projectName, err := opts.toProjectName()
 	if err != nil {
 		return err
 	}
-	containers, err := c.ComposeService().Top(ctx, projectName, services)
+	containers, err := backend.Top(ctx, projectName, services)
 	if err != nil {
 		return err
 	}

+ 20 - 24
cli/cmd/compose/up.go

@@ -33,7 +33,6 @@ import (
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
 
-	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/context/store"
 	"github.com/docker/compose-cli/api/progress"
@@ -140,7 +139,7 @@ func (opts upOptions) apply(project *types.Project, services []string) error {
 	return nil
 }
 
-func upCommand(p *projectOptions, contextType string) *cobra.Command {
+func upCommand(p *projectOptions, contextType string, backend compose.Service) *cobra.Command {
 	opts := upOptions{
 		composeOptions: &composeOptions{
 			projectOptions: p,
@@ -149,8 +148,10 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
 	upCmd := &cobra.Command{
 		Use:   "up [SERVICE...]",
 		Short: "Create and start containers",
-		RunE: func(cmd *cobra.Command, args []string) error {
+		PreRun: func(cmd *cobra.Command, args []string) {
 			opts.timeChanged = cmd.Flags().Changed("timeout")
+		},
+		RunE: Adapt(func(ctx context.Context, args []string) error {
 			switch contextType {
 			case store.LocalContextType, store.DefaultContextType, store.EcsLocalSimulationContextType:
 				if opts.exitCodeFrom != "" {
@@ -168,11 +169,11 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
 				if opts.recreateDeps && opts.noRecreate {
 					return fmt.Errorf("--always-recreate-deps and --no-recreate are incompatible")
 				}
-				return runCreateStart(cmd.Context(), opts, args)
+				return runCreateStart(ctx, backend, opts, args)
 			default:
-				return runUp(cmd.Context(), opts, args)
+				return runUp(ctx, backend, opts, args)
 			}
-		},
+		}),
 	}
 	flags := upCmd.Flags()
 	flags.StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables")
@@ -204,8 +205,8 @@ func upCommand(p *projectOptions, contextType string) *cobra.Command {
 	return upCmd
 }
 
-func runUp(ctx context.Context, opts upOptions, services []string) error {
-	c, project, err := setup(ctx, *opts.composeOptions, services)
+func runUp(ctx context.Context, backend compose.Service, opts upOptions, services []string) error {
+	project, err := setup(*opts.composeOptions, services)
 	if err != nil {
 		return err
 	}
@@ -216,7 +217,7 @@ func runUp(ctx context.Context, opts upOptions, services []string) error {
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		return "", c.ComposeService().Up(ctx, project, compose.UpOptions{
+		return "", backend.Up(ctx, project, compose.UpOptions{
 			Detach:    opts.Detach,
 			QuietPull: opts.quietPull,
 		})
@@ -224,8 +225,8 @@ func runUp(ctx context.Context, opts upOptions, services []string) error {
 	return err
 }
 
-func runCreateStart(ctx context.Context, opts upOptions, services []string) error {
-	c, project, err := setup(ctx, *opts.composeOptions, services)
+func runCreateStart(ctx context.Context, backend compose.Service, opts upOptions, services []string) error {
+	project, err := setup(*opts.composeOptions, services)
 	if err != nil {
 		return err
 	}
@@ -240,7 +241,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 	}
 
 	_, err = progress.Run(ctx, func(ctx context.Context) (string, error) {
-		err := c.ComposeService().Create(ctx, project, compose.CreateOptions{
+		err := backend.Create(ctx, project, compose.CreateOptions{
 			Services:             services,
 			RemoveOrphans:        opts.removeOrphans,
 			Recreate:             opts.recreateStrategy(),
@@ -253,7 +254,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 			return "", err
 		}
 		if opts.Detach {
-			err = c.ComposeService().Start(ctx, project, compose.StartOptions{})
+			err = backend.Start(ctx, project, compose.StartOptions{})
 		}
 		return "", err
 	})
@@ -285,10 +286,10 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 		_, err := progress.Run(ctx, func(ctx context.Context) (string, error) {
 			go func() {
 				<-signalChan
-				c.ComposeService().Kill(ctx, project, compose.KillOptions{}) // nolint:errcheck
+				backend.Kill(ctx, project, compose.KillOptions{}) // nolint:errcheck
 			}()
 
-			return "", c.ComposeService().Stop(ctx, project, compose.StopOptions{})
+			return "", backend.Stop(ctx, project, compose.StopOptions{})
 		})
 		return err
 	}
@@ -311,7 +312,7 @@ func runCreateStart(ctx context.Context, opts upOptions, services []string) erro
 		return err
 	})
 
-	err = c.ComposeService().Start(ctx, project, compose.StartOptions{
+	err = backend.Start(ctx, project, compose.StartOptions{
 		Attach: func(event compose.ContainerEvent) {
 			queue <- event
 		},
@@ -351,15 +352,10 @@ func setServiceScale(project *types.Project, name string, replicas int) error {
 	return fmt.Errorf("unknown service %q", name)
 }
 
-func setup(ctx context.Context, opts composeOptions, services []string) (*client.Client, *types.Project, error) {
-	c, err := client.New(ctx)
-	if err != nil {
-		return nil, nil, err
-	}
-
+func setup(opts composeOptions, services []string) (*types.Project, error) {
 	project, err := opts.toProject(services)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
 	if opts.DomainName != "" {
@@ -397,7 +393,7 @@ func setup(ctx context.Context, opts composeOptions, services []string) (*client
 		project.Services = services
 	}
 
-	return c, project, nil
+	return project, nil
 }
 
 type printer struct {

+ 35 - 0
cli/config/flags.go

@@ -17,9 +17,11 @@
 package config
 
 import (
+	"fmt"
 	"os"
 	"path/filepath"
 
+	"github.com/pkg/errors"
 	"github.com/spf13/pflag"
 
 	"github.com/docker/compose-cli/api/config"
@@ -44,3 +46,36 @@ func confDir() string {
 	home, _ := os.UserHomeDir()
 	return filepath.Join(home, config.ConfigFileDir)
 }
+
+// GetCurrentContext get current context based on opts, env vars
+func GetCurrentContext(contextOpt string, configDir string, hosts []string) string {
+	// host and context flags cannot be both set at the same time -- the local backend enforces this when resolving hostname
+	// -H flag disables context --> set default as current
+	if len(hosts) > 0 {
+		return "default"
+	}
+	// DOCKER_HOST disables context --> set default as current
+	if _, present := os.LookupEnv("DOCKER_HOST"); present {
+		return "default"
+	}
+	res := contextOpt
+	if res == "" {
+		// check if DOCKER_CONTEXT env variable was set
+		if _, present := os.LookupEnv("DOCKER_CONTEXT"); present {
+			res = os.Getenv("DOCKER_CONTEXT")
+		}
+
+		if res == "" {
+			config, err := config.LoadFile(configDir)
+			if err != nil {
+				fmt.Fprintln(os.Stderr, errors.Wrap(err, "WARNING"))
+				return "default"
+			}
+			res = config.CurrentContext
+		}
+	}
+	if res == "" {
+		res = "default"
+	}
+	return res
+}

+ 61 - 0
cli/config/flags_test.go

@@ -0,0 +1,61 @@
+/*
+   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 config
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"gotest.tools/v3/assert"
+
+	"github.com/docker/compose-cli/api/config"
+)
+
+var contextSetConfig = []byte(`{
+	"currentContext": "some-context"
+}`)
+
+func TestDetermineCurrentContext(t *testing.T) {
+	d, err := ioutil.TempDir("", "")
+	// nolint errcheck
+	defer os.RemoveAll(d)
+	assert.NilError(t, err)
+	err = ioutil.WriteFile(filepath.Join(d, config.ConfigFileName), contextSetConfig, 0644)
+	assert.NilError(t, err)
+
+	// If nothing set, fallback to default
+	c := GetCurrentContext("", "", []string{})
+	assert.Equal(t, c, "default")
+
+	// If context flag set, use that
+	c = GetCurrentContext("other-context", "", []string{})
+	assert.Equal(t, c, "other-context")
+
+	// If no context flag, use config
+	c = GetCurrentContext("", d, []string{})
+	assert.Equal(t, c, "some-context")
+
+	// Ensure context flag overrides config
+	c = GetCurrentContext("other-context", d, []string{})
+	assert.Equal(t, "other-context", c)
+
+	// Ensure host flag overrides context
+	c = GetCurrentContext("other-context", d, []string{"hostname"})
+	assert.Equal(t, "default", c)
+}

+ 5 - 59
cli/main.go

@@ -29,9 +29,6 @@ import (
 	"time"
 
 	"github.com/docker/cli/cli"
-	"github.com/docker/cli/cli/command"
-	cliconfig "github.com/docker/cli/cli/config"
-	cliflags "github.com/docker/cli/cli/flags"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -48,6 +45,7 @@ import (
 	"github.com/docker/compose-cli/cli/cmd/logout"
 	"github.com/docker/compose-cli/cli/cmd/run"
 	"github.com/docker/compose-cli/cli/cmd/volume"
+	cliconfig "github.com/docker/compose-cli/cli/config"
 	"github.com/docker/compose-cli/cli/metrics"
 	"github.com/docker/compose-cli/cli/mobycli"
 	cliopts "github.com/docker/compose-cli/cli/options"
@@ -62,7 +60,6 @@ import (
 
 var (
 	contextAgnosticCommands = map[string]struct{}{
-		"compose":          {},
 		"context":          {},
 		"login":            {},
 		"logout":           {},
@@ -198,7 +195,7 @@ func main() {
 	configDir := opts.Config
 	config.WithDir(configDir)
 
-	currentContext := determineCurrentContext(opts.Context, configDir, opts.Hosts)
+	currentContext := cliconfig.GetCurrentContext(opts.Context, configDir, opts.Hosts)
 	apicontext.WithCurrentContext(currentContext)
 
 	s, err := store.New(configDir)
@@ -221,7 +218,7 @@ func main() {
 
 	root.AddCommand(
 		run.Command(ctype),
-		compose.Command(ctype),
+		compose.RootCommand(ctype, service.ComposeService()),
 		volume.Command(ctype),
 	)
 
@@ -234,27 +231,7 @@ func main() {
 func getBackend(ctype string, configDir string, opts cliopts.GlobalOpts) (backend.Service, error) {
 	switch ctype {
 	case store.DefaultContextType, store.LocalContextType:
-		configFile, err := cliconfig.Load(configDir)
-		if err != nil {
-			return nil, err
-		}
-		options := cliflags.CommonOptions{
-			Context:  opts.Context,
-			Debug:    opts.Debug,
-			Hosts:    opts.Hosts,
-			LogLevel: opts.LogLevel,
-		}
-
-		if opts.TLSVerify {
-			options.TLS = opts.TLS
-			options.TLSVerify = opts.TLSVerify
-			options.TLSOptions = opts.TLSOptions
-		}
-		apiClient, err := command.NewAPIClientFromFlags(&options, configFile)
-		if err != nil {
-			return nil, err
-		}
-		return local.NewService(apiClient), nil
+		return local.GetLocalBackend(configDir, opts)
 	}
 	service, err := backend.Get(ctype)
 	if errdefs.IsNotFoundError(err) {
@@ -311,6 +288,7 @@ func exit(ctx string, err error, ctype string) {
 	}
 
 	if compose.Warning != "" {
+		logrus.Warn(err)
 		fmt.Fprintln(os.Stderr, compose.Warning)
 	}
 
@@ -354,38 +332,6 @@ func newSigContext() (context.Context, func()) {
 	return ctx, cancel
 }
 
-func determineCurrentContext(flag string, configDir string, hosts []string) string {
-	// host and context flags cannot be both set at the same time -- the local backend enforces this when resolving hostname
-	// -H flag disables context --> set default as current
-	if len(hosts) > 0 {
-		return "default"
-	}
-	// DOCKER_HOST disables context --> set default as current
-	if _, present := os.LookupEnv("DOCKER_HOST"); present {
-		return "default"
-	}
-	res := flag
-	if res == "" {
-		// check if DOCKER_CONTEXT env variable was set
-		if _, present := os.LookupEnv("DOCKER_CONTEXT"); present {
-			res = os.Getenv("DOCKER_CONTEXT")
-		}
-
-		if res == "" {
-			config, err := config.LoadFile(configDir)
-			if err != nil {
-				fmt.Fprintln(os.Stderr, errors.Wrap(err, "WARNING"))
-				return "default"
-			}
-			res = config.CurrentContext
-		}
-	}
-	if res == "" {
-		res = "default"
-	}
-	return res
-}
-
 func walk(c *cobra.Command, f func(*cobra.Command)) {
 	f(c)
 	for _, c := range c.Commands() {

+ 0 - 36
cli/main_test.go

@@ -17,53 +17,17 @@
 package main
 
 import (
-	"io/ioutil"
 	"os"
-	"path/filepath"
 	"testing"
 
 	"gotest.tools/v3/assert"
 
-	"github.com/docker/compose-cli/api/config"
 	"github.com/docker/compose-cli/cli/cmd"
 	"github.com/docker/compose-cli/cli/cmd/context"
 	"github.com/docker/compose-cli/cli/cmd/login"
 	"github.com/docker/compose-cli/cli/cmd/run"
 )
 
-var contextSetConfig = []byte(`{
-	"currentContext": "some-context"
-}`)
-
-func TestDetermineCurrentContext(t *testing.T) {
-	d, err := ioutil.TempDir("", "")
-	// nolint errcheck
-	defer os.RemoveAll(d)
-	assert.NilError(t, err)
-	err = ioutil.WriteFile(filepath.Join(d, config.ConfigFileName), contextSetConfig, 0644)
-	assert.NilError(t, err)
-
-	// If nothing set, fallback to default
-	c := determineCurrentContext("", "", []string{})
-	assert.Equal(t, c, "default")
-
-	// If context flag set, use that
-	c = determineCurrentContext("other-context", "", []string{})
-	assert.Equal(t, c, "other-context")
-
-	// If no context flag, use config
-	c = determineCurrentContext("", d, []string{})
-	assert.Equal(t, c, "some-context")
-
-	// Ensure context flag overrides config
-	c = determineCurrentContext("other-context", d, []string{})
-	assert.Equal(t, "other-context", c)
-
-	// Ensure host flag overrides context
-	c = determineCurrentContext("other-context", d, []string{"hostname"})
-	assert.Equal(t, "default", c)
-}
-
 func TestCheckOwnCommand(t *testing.T) {
 	assert.Assert(t, isContextAgnosticCommand(login.Command()))
 	assert.Assert(t, isContextAgnosticCommand(context.Command()))

+ 23 - 0
cli/metrics/definitions.go

@@ -55,3 +55,26 @@ var (
 	// PullFailure failure while pulling image
 	PullFailure = FailureCategory{MetricsStatus: PullFailureStatus, ExitCode: 18}
 )
+
+//ByExitCode retrieve FailureCategory based on command exit code
+func ByExitCode(exitCode int) FailureCategory {
+	switch exitCode {
+	case 0:
+		return FailureCategory{MetricsStatus: SuccessStatus, ExitCode: 0}
+	case 14:
+		return FileNotFoundFailure
+	case 15:
+		return ComposeParseFailure
+	case 16:
+		return CommandSyntaxFailure
+	case 17:
+		return BuildFailure
+	case 18:
+		return PullFailure
+	case 130:
+		return FailureCategory{MetricsStatus: CanceledStatus, ExitCode: exitCode}
+	default:
+		return FailureCategory{MetricsStatus: FailureStatus, ExitCode: exitCode}
+	}
+
+}

+ 2 - 6
cli/mobycli/exec.go

@@ -68,12 +68,8 @@ func Exec(root *cobra.Command) {
 	if err != nil {
 		if exiterr, ok := err.(*exec.ExitError); ok {
 			exitCode := exiterr.ExitCode()
-			if exitCode == 130 {
-				metrics.Track(store.DefaultContextType, os.Args[1:], metrics.CanceledStatus)
-			} else {
-				metrics.Track(store.DefaultContextType, os.Args[1:], metrics.FailureStatus)
-			}
-			os.Exit(exiterr.ExitCode())
+			metrics.Track(store.DefaultContextType, os.Args[1:], metrics.ByExitCode(exitCode).MetricsStatus)
+			os.Exit(exitCode)
 		}
 		metrics.Track(store.DefaultContextType, os.Args[1:], metrics.FailureStatus)
 		fmt.Fprintln(os.Stderr, err)

+ 1 - 1
docs/yaml/main/generate.go

@@ -35,7 +35,7 @@ const descriptionSourcePath = "docs/reference/"
 
 func generateCliYaml(opts *options) error {
 	cmd := &cobra.Command{Use: "docker"}
-	cmd.AddCommand(compose.Command("local"))
+	cmd.AddCommand(compose.RootCommand("local", nil))
 	disableFlagsInUseLine(cmd)
 	source := filepath.Join(opts.source, descriptionSourcePath)
 	if err := loadLongDescription(cmd, source); err != nil {

+ 4 - 2
ecs/local/backend.go

@@ -17,8 +17,9 @@
 package local
 
 import (
-	local_compose "github.com/docker/compose-cli/local/compose"
+	"os"
 
+	cliconfig "github.com/docker/cli/cli/config"
 	"github.com/docker/docker/client"
 
 	"github.com/docker/compose-cli/api/backend"
@@ -29,6 +30,7 @@ import (
 	"github.com/docker/compose-cli/api/resources"
 	"github.com/docker/compose-cli/api/secrets"
 	"github.com/docker/compose-cli/api/volumes"
+	local_compose "github.com/docker/compose-cli/local/compose"
 )
 
 const backendType = store.EcsLocalSimulationContextType
@@ -50,7 +52,7 @@ func service() (backend.Service, error) {
 
 	return &ecsLocalSimulation{
 		moby:    apiClient,
-		compose: local_compose.NewComposeService(apiClient),
+		compose: local_compose.NewComposeService(apiClient, cliconfig.LoadDefaultConfigFile(os.Stderr)),
 	}, nil
 }
 

+ 33 - 1
local/backend.go

@@ -17,6 +17,11 @@
 package local
 
 import (
+	"os"
+
+	"github.com/docker/cli/cli/command"
+	cliconfig "github.com/docker/cli/cli/config"
+	cliflags "github.com/docker/cli/cli/flags"
 	"github.com/docker/docker/client"
 
 	"github.com/docker/compose-cli/api/backend"
@@ -25,6 +30,7 @@ import (
 	"github.com/docker/compose-cli/api/resources"
 	"github.com/docker/compose-cli/api/secrets"
 	"github.com/docker/compose-cli/api/volumes"
+	cliopts "github.com/docker/compose-cli/cli/options"
 	local_compose "github.com/docker/compose-cli/local/compose"
 )
 
@@ -36,11 +42,37 @@ type local struct {
 
 // NewService build a backend for "local" context, using Docker API client
 func NewService(apiClient client.APIClient) backend.Service {
+	file := cliconfig.LoadDefaultConfigFile(os.Stderr)
 	return &local{
 		containerService: &containerService{apiClient},
 		volumeService:    &volumeService{apiClient},
-		composeService:   local_compose.NewComposeService(apiClient),
+		composeService:   local_compose.NewComposeService(apiClient, file),
+	}
+}
+
+// GetLocalBackend initialize local backend
+func GetLocalBackend(configDir string, opts cliopts.GlobalOpts) (backend.Service, error) {
+	configFile, err := cliconfig.Load(configDir)
+	if err != nil {
+		return nil, err
+	}
+	options := cliflags.CommonOptions{
+		Context:  opts.Context,
+		Debug:    opts.Debug,
+		Hosts:    opts.Hosts,
+		LogLevel: opts.LogLevel,
+	}
+
+	if opts.TLSVerify {
+		options.TLS = opts.TLS
+		options.TLSVerify = opts.TLSVerify
+		options.TLSOptions = opts.TLSOptions
+	}
+	apiClient, err := command.NewAPIClientFromFlags(&options, configFile)
+	if err != nil {
+		return nil, err
 	}
+	return NewService(apiClient), nil
 }
 
 func (s *local) ContainerService() containers.Service {

+ 1 - 8
local/compose/build.go

@@ -28,13 +28,11 @@ import (
 	"github.com/docker/buildx/driver"
 	_ "github.com/docker/buildx/driver/docker" // required to get default driver registered
 	"github.com/docker/buildx/util/progress"
-	cliconfig "github.com/docker/cli/cli/config"
 	moby "github.com/docker/docker/api/types"
 	bclient "github.com/moby/buildkit/client"
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
 
 	"github.com/docker/compose-cli/api/compose"
-	"github.com/docker/compose-cli/api/config"
 	composeprogress "github.com/docker/compose-cli/api/progress"
 	"github.com/docker/compose-cli/cli/metrics"
 	"github.com/docker/compose-cli/utils"
@@ -195,12 +193,7 @@ func (s *composeService) build(ctx context.Context, project *types.Project, opts
 	}
 	const drivername = "default"
 
-	configFile, err := cliconfig.Load(config.Dir())
-	if err != nil {
-		return nil, err
-	}
-
-	d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, configFile, nil, nil, "", nil, nil, project.WorkingDir)
+	d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, "", nil, nil, project.WorkingDir)
 	if err != nil {
 		return nil, err
 	}

+ 6 - 3
local/compose/compose.go

@@ -26,20 +26,23 @@ import (
 	"github.com/docker/compose-cli/api/errdefs"
 
 	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/cli/cli/config/configfile"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
 	"github.com/sanathkr/go-yaml"
 )
 
 // NewComposeService create a local implementation of the compose.Service API
-func NewComposeService(apiClient client.APIClient) compose.Service {
+func NewComposeService(apiClient client.APIClient, configFile *configfile.ConfigFile) compose.Service {
 	return &composeService{
-		apiClient: apiClient,
+		apiClient:  apiClient,
+		configFile: configFile,
 	}
 }
 
 type composeService struct {
-	apiClient client.APIClient
+	apiClient  client.APIClient
+	configFile *configfile.ConfigFile
 }
 
 func (s *composeService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error {

+ 1 - 7
local/compose/pull.go

@@ -27,23 +27,17 @@ import (
 	"github.com/compose-spec/compose-go/types"
 	"github.com/distribution/distribution/v3/reference"
 	"github.com/docker/buildx/driver"
-	cliconfig "github.com/docker/cli/cli/config"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/pkg/jsonmessage"
 	"github.com/docker/docker/registry"
 	"golang.org/x/sync/errgroup"
 
 	"github.com/docker/compose-cli/api/compose"
-	"github.com/docker/compose-cli/api/config"
 	"github.com/docker/compose-cli/api/progress"
 	"github.com/docker/compose-cli/cli/metrics"
 )
 
 func (s *composeService) Pull(ctx context.Context, project *types.Project, opts compose.PullOptions) error {
-	configFile, err := cliconfig.Load(config.Dir())
-	if err != nil {
-		return err
-	}
 	info, err := s.apiClient.Info(ctx)
 	if err != nil {
 		return err
@@ -67,7 +61,7 @@ func (s *composeService) Pull(ctx context.Context, project *types.Project, opts
 			continue
 		}
 		eg.Go(func() error {
-			err := s.pullServiceImage(ctx, service, info, configFile, w)
+			err := s.pullServiceImage(ctx, service, info, s.configFile, w)
 			if err != nil {
 				if !opts.IgnoreFailures {
 					return err

+ 21 - 0
local/e2e/compose/compose_test.go

@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net/http"
 	"os"
+	"path/filepath"
 	"regexp"
 	"strings"
 	"testing"
@@ -131,6 +132,26 @@ func TestLocalComposeUp(t *testing.T) {
 	})
 }
 
+func TestComposeUsingCliPlugin(t *testing.T) {
+	c := NewParallelE2eCLI(t, binDir)
+
+	err := os.Remove(filepath.Join(c.ConfigDir, "cli-plugins", "docker-compose"))
+	assert.NilError(t, err)
+	res := c.RunDockerOrExitError("compose", "ls")
+	res.Assert(t, icmd.Expected{Err: "'compose' is not a docker command", ExitCode: 1})
+}
+
+func TestComposeCliPluginWithoutCloudIntegration(t *testing.T) {
+	c := NewParallelE2eCLI(t, binDir)
+
+	err := os.Remove(filepath.Join(binDir, "docker"))
+	assert.NilError(t, err)
+	err = os.Rename(filepath.Join(binDir, "com.docker.cli"), filepath.Join(binDir, "docker"))
+	assert.NilError(t, err)
+	res := c.RunDockerOrExitError("compose", "ls")
+	res.Assert(t, icmd.Expected{Out: "NAME                STATUS", ExitCode: 0})
+}
+
 func TestComposePull(t *testing.T) {
 	c := NewParallelE2eCLI(t, binDir)
 

+ 3 - 0
local/e2e/compose/fixtures/build-infinite/docker-compose.yml

@@ -0,0 +1,3 @@
+services:
+    service1:
+        build: service1

+ 17 - 0
local/e2e/compose/fixtures/build-infinite/service1/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 busybox
+
+RUN sleep infinity

+ 70 - 0
local/e2e/compose/metrics_test.go

@@ -17,7 +17,11 @@
 package e2e
 
 import (
+	"bytes"
 	"fmt"
+	"os/exec"
+	"strings"
+	"syscall"
 	"testing"
 	"time"
 
@@ -84,3 +88,69 @@ func TestComposeMetrics(t *testing.T) {
 		}, usage)
 	})
 }
+
+func TestComposeCancel(t *testing.T) {
+	c := NewParallelE2eCLI(t, binDir)
+	s := NewMetricsServer(c.MetricsSocket())
+	s.Start()
+	defer s.Stop()
+
+	started := false
+
+	for i := 0; i < 30; i++ {
+		c.RunDockerCmd("help", "ps")
+		if len(s.GetUsage()) > 0 {
+			started = true
+			fmt.Printf("    [%s] Server up in %d ms\n", t.Name(), i*100)
+			break
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
+	assert.Assert(t, started, "Metrics mock server not available after 3 secs")
+
+	t.Run("metrics on cancel Compose build", func(t *testing.T) {
+		s.ResetUsage()
+
+		c.RunDockerCmd("compose", "ls")
+		buildProjectPath := "../compose/fixtures/build-infinite/docker-compose.yml"
+
+		// require a separate groupID from the process running tests, in order to simulate ctrl+C from a terminal.
+		// sending kill signal
+		cmd, stdout, stderr, err := StartWithNewGroupID(c.NewDockerCmd("compose", "-f", buildProjectPath, "build", "--progress", "plain"))
+		assert.NilError(t, err)
+
+		c.WaitForCondition(func() (bool, string) {
+			out := stdout.String()
+			errors := stderr.String()
+			return strings.Contains(out, "RUN sleep infinity"), fmt.Sprintf("'RUN sleep infinity' not found in : \n%s\nStderr: \n%s\n", out, errors)
+		}, 30*time.Second, 1*time.Second)
+
+		err = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) // simulate Ctrl-C : send signal to processGroup, children will have same groupId by default
+
+		assert.NilError(t, err)
+		c.WaitForCondition(func() (bool, string) {
+			out := stdout.String()
+			errors := stderr.String()
+			return strings.Contains(out, "CANCELED"), fmt.Sprintf("'CANCELED' not found in : \n%s\nStderr: \n%s\n", out, errors)
+		}, 10*time.Second, 1*time.Second)
+
+		usage := s.GetUsage()
+		assert.DeepEqual(t, []string{
+			`{"command":"compose ls","context":"moby","source":"cli","status":"success"}`,
+			`{"command":"compose build","context":"moby","source":"cli","status":"canceled"}`,
+		}, usage)
+	})
+}
+
+func StartWithNewGroupID(command icmd.Cmd) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer, error) {
+	cmd := exec.Command(command.Command[0], command.Command[1:]...)
+	cmd.Env = command.Env
+	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+
+	var stdout bytes.Buffer
+	var stderr bytes.Buffer
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err := cmd.Start()
+	return cmd, &stdout, &stderr, err
+}

+ 66 - 0
main.go

@@ -0,0 +1,66 @@
+/*
+   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 main
+
+import (
+	"strings"
+
+	dockercli "github.com/docker/cli/cli"
+	"github.com/docker/cli/cli-plugins/manager"
+	"github.com/docker/cli/cli-plugins/plugin"
+	"github.com/docker/cli/cli/command"
+	"github.com/spf13/cobra"
+
+	api "github.com/docker/compose-cli/api/compose"
+	"github.com/docker/compose-cli/api/context/store"
+	"github.com/docker/compose-cli/cli/cmd/compose"
+	"github.com/docker/compose-cli/cli/metrics"
+	"github.com/docker/compose-cli/internal"
+	impl "github.com/docker/compose-cli/local/compose"
+)
+
+func main() {
+	plugin.Run(func(dockerCli command.Cli) *cobra.Command {
+		lazyInit := api.ServiceDelegator{
+			Delegate: api.NoImpl{},
+		}
+		cmd := compose.RootCommand(store.DefaultContextType, &lazyInit)
+		originalPreRun := cmd.PersistentPreRunE
+		cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+			if err := plugin.PersistentPreRunE(cmd, args); err != nil {
+				return err
+			}
+			lazyInit.Delegate = impl.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile())
+			if originalPreRun != nil {
+				return originalPreRun(cmd, args)
+			}
+			return nil
+		}
+		cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
+			return dockercli.StatusError{
+				StatusCode: metrics.CommandSyntaxFailure.ExitCode,
+				Status:     err.Error(),
+			}
+		})
+		return cmd
+	},
+		manager.Metadata{
+			SchemaVersion: "0.1.0",
+			Vendor:        "Docker Inc.",
+			Version:       strings.TrimPrefix(internal.Version, "v"),
+		})
+}

+ 26 - 3
utils/e2e/framework.go

@@ -85,6 +85,17 @@ func newE2eCLI(t *testing.T, binDir string) *E2eCLI {
 		_ = os.RemoveAll(d)
 	})
 
+	_ = os.MkdirAll(filepath.Join(d, "cli-plugins"), 0755)
+	composePluginFile := "docker-compose"
+	if runtime.GOOS == "windows" {
+		composePluginFile += ".exe"
+	}
+	composePlugin, _ := findExecutable(composePluginFile, []string{"../../bin", "../../../bin"})
+	err = CopyFile(composePlugin, filepath.Join(d, "cli-plugins", composePluginFile))
+	if err != nil {
+		panic(err)
+	}
+
 	return &E2eCLI{binDir, d, t}
 }
 
@@ -117,7 +128,7 @@ func SetupExistingCLI() (string, func(), error) {
 		return "", nil, err
 	}
 
-	bin, err := findExecutable([]string{"../../bin", "../../../bin"})
+	bin, err := findExecutable(DockerExecutableName, []string{"../../bin", "../../../bin"})
 	if err != nil {
 		return "", nil, err
 	}
@@ -133,9 +144,9 @@ func SetupExistingCLI() (string, func(), error) {
 	return d, cleanup, nil
 }
 
-func findExecutable(paths []string) (string, error) {
+func findExecutable(executableName string, paths []string) (string, error) {
 	for _, p := range paths {
-		bin, err := filepath.Abs(path.Join(p, DockerExecutableName))
+		bin, err := filepath.Abs(path.Join(p, executableName))
 		if err != nil {
 			return "", err
 		}
@@ -241,6 +252,18 @@ func (c *E2eCLI) WaitForCmdResult(command icmd.Cmd, predicate func(*icmd.Result)
 	poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
 }
 
+// WaitForCondition wait for predicate to execute to true
+func (c *E2eCLI) WaitForCondition(predicate func() (bool, string), timeout time.Duration, delay time.Duration) {
+	checkStopped := func(logt poll.LogT) poll.Result {
+		pass, description := predicate()
+		if !pass {
+			return poll.Continue("Condition not met: %q", description)
+		}
+		return poll.Success()
+	}
+	poll.WaitOn(c.test, checkStopped, poll.WithDelay(delay), poll.WithTimeout(timeout))
+}
+
 // PathEnvVar returns path (os sensitive) for running test
 func (c *E2eCLI) PathEnvVar() string {
 	path := c.BinDir + ":" + os.Getenv("PATH")