소스 검색

Merge pull request #9198 from ndeloof/dockerCli_stdout

composeService to use dockerCli's In/Out/Err streams
Guillaume Lours 3 년 전
부모
커밋
7036cda306

+ 0 - 22
cmd/compose/exec.go

@@ -18,11 +18,8 @@ package compose
 
 import (
 	"context"
-	"fmt"
-	"os"
 
 	"github.com/compose-spec/compose-go/types"
-	"github.com/containerd/console"
 	"github.com/docker/cli/cli"
 	"github.com/docker/compose/v2/pkg/api"
 	"github.com/docker/compose/v2/pkg/compose"
@@ -105,27 +102,8 @@ func runExec(ctx context.Context, backend api.Service, opts execOpts) error {
 		Index:       opts.index,
 		Detach:      opts.detach,
 		WorkingDir:  opts.workingDir,
-
-		Stdin:  os.Stdin,
-		Stdout: os.Stdout,
-		Stderr: os.Stderr,
 	}
 
-	if execOpts.Tty {
-		con := console.Current()
-		if err := con.SetRaw(); err != nil {
-			return err
-		}
-		defer func() {
-			if err := con.Reset(); err != nil {
-				fmt.Println("Unable to close the console")
-			}
-		}()
-
-		execOpts.Stdin = con
-		execOpts.Stdout = con
-		execOpts.Stderr = con
-	}
 	exitCode, err := backend.Exec(ctx, projectName, execOpts)
 	if exitCode != 0 {
 		errMsg := ""

+ 0 - 4
cmd/compose/run.go

@@ -19,7 +19,6 @@ package compose
 import (
 	"context"
 	"fmt"
-	"os"
 	"strings"
 
 	cgo "github.com/compose-spec/compose-go/cli"
@@ -207,9 +206,6 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op
 		Command:           opts.Command,
 		Detach:            opts.Detach,
 		AutoRemove:        opts.Remove,
-		Stdin:             os.Stdin,
-		Stdout:            os.Stdout,
-		Stderr:            os.Stderr,
 		Tty:               !opts.noTty,
 		WorkingDir:        opts.workdir,
 		User:              opts.user,

+ 1 - 1
cmd/main.go

@@ -46,7 +46,7 @@ func pluginMain() {
 			if err := plugin.PersistentPreRunE(cmd, args); err != nil {
 				return err
 			}
-			lazyInit.WithService(compose.NewComposeService(dockerCli.Client(), dockerCli.ConfigFile()))
+			lazyInit.WithService(compose.NewComposeService(dockerCli))
 			if originalPreRun != nil {
 				return originalPreRun(cmd, args)
 			}

+ 1 - 1
go.mod

@@ -32,6 +32,7 @@ require (
 	github.com/spf13/cobra v1.3.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.7.0
+	github.com/theupdateframework/notary v0.6.1
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	gotest.tools v2.2.0+incompatible
 	gotest.tools/v3 v3.1.0
@@ -94,7 +95,6 @@ require (
 	github.com/qri-io/jsonpointer v0.1.0 // indirect
 	github.com/qri-io/jsonschema v0.1.1 // indirect
 	github.com/sergi/go-diff v1.1.0 // indirect
-	github.com/theupdateframework/notary v0.6.1 // indirect
 	github.com/tonistiigi/fsutil v0.0.0-20210818161904-4442383b5028 // indirect
 	github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
 	github.com/tonistiigi/vt100 v0.0.0-20210615222946-8066bb97264f // indirect

+ 0 - 4
pkg/api/api.go

@@ -19,7 +19,6 @@ package api
 import (
 	"context"
 	"fmt"
-	"io"
 	"strings"
 	"time"
 
@@ -216,9 +215,6 @@ type RunOptions struct {
 	Entrypoint        []string
 	Detach            bool
 	AutoRemove        bool
-	Stdin             io.ReadCloser
-	Stdout            io.WriteCloser
-	Stderr            io.WriteCloser
 	Tty               bool
 	WorkingDir        string
 	User              string

+ 2 - 2
pkg/compose/attach.go

@@ -137,7 +137,7 @@ func (s *composeService) attachContainerStreams(ctx context.Context, container s
 func (s *composeService) getContainerStreams(ctx context.Context, container string) (io.WriteCloser, io.ReadCloser, error) {
 	var stdout io.ReadCloser
 	var stdin io.WriteCloser
-	cnx, err := s.apiClient.ContainerAttach(ctx, container, moby.ContainerAttachOptions{
+	cnx, err := s.apiClient().ContainerAttach(ctx, container, moby.ContainerAttachOptions{
 		Stream: true,
 		Stdin:  true,
 		Stdout: true,
@@ -151,7 +151,7 @@ func (s *composeService) getContainerStreams(ctx context.Context, container stri
 	}
 
 	// Fallback to logs API
-	logs, err := s.apiClient.ContainerLogs(ctx, container, moby.ContainerLogsOptions{
+	logs, err := s.apiClient().ContainerLogs(ctx, container, moby.ContainerLogsOptions{
 		ShowStdout: true,
 		ShowStderr: true,
 		Follow:     true,

+ 2 - 3
pkg/compose/build.go

@@ -19,7 +19,6 @@ package compose
 import (
 	"context"
 	"fmt"
-	"os"
 	"path/filepath"
 
 	"github.com/compose-spec/compose-go/types"
@@ -193,7 +192,7 @@ func (s *composeService) getLocalImagesDigests(ctx context.Context, project *typ
 }
 
 func (s *composeService) serverInfo(ctx context.Context) (command.ServerInfo, error) {
-	ping, err := s.apiClient.Ping(ctx)
+	ping, err := s.apiClient().Ping(ctx)
 	if err != nil {
 		return command.ServerInfo{}, err
 	}
@@ -258,7 +257,7 @@ func (s *composeService) toBuildOptions(project *types.Project, service types.Se
 		NetworkMode: service.Build.Network,
 		ExtraHosts:  service.Build.ExtraHosts,
 		Session: []session.Attachable{
-			authprovider.NewDockerAuthProvider(os.Stderr),
+			authprovider.NewDockerAuthProvider(s.stderr()),
 		},
 	}, nil
 }

+ 2 - 2
pkg/compose/build_buildkit.go

@@ -29,7 +29,7 @@ import (
 
 func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Project, opts map[string]build.Options, mode string) (map[string]string, error) {
 	const drivername = "default"
-	d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient, s.configFile, nil, nil, nil, nil, nil, project.WorkingDir)
+	d, err := driver.GetDriver(ctx, drivername, nil, s.apiClient(), s.configFile(), nil, nil, nil, nil, nil, project.WorkingDir)
 	if err != nil {
 		return nil, err
 	}
@@ -48,7 +48,7 @@ func (s *composeService) doBuildBuildkit(ctx context.Context, project *types.Pro
 	w := xprogress.NewPrinter(progressCtx, os.Stdout, mode)
 
 	// We rely on buildx "docker" builder integrated in docker engine, so don't need a DockerAPI here
-	response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile.Filename), w)
+	response, err := build.Build(ctx, driverInfo, opts, nil, filepath.Dir(s.configFile().Filename), w)
 	errW := w.Wait()
 	if err == nil {
 		err = errW

+ 7 - 7
pkg/compose/build_classic.go

@@ -69,8 +69,8 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
 
 	dockerfileName := options.Inputs.DockerfilePath
 	specifiedContext := options.Inputs.ContextPath
-	progBuff := os.Stdout
-	buildBuff := os.Stdout
+	progBuff := s.stdout()
+	buildBuff := s.stdout()
 	if options.ImageIDFile != "" {
 		// Avoid leaving a stale file if we eventually fail
 		if err := os.Remove(options.ImageIDFile); err != nil && !os.IsNotExist(err) {
@@ -155,7 +155,7 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
 		body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
 	}
 
-	configFile := s.configFile
+	configFile := s.configFile()
 	creds, err := configFile.GetAllCredentials()
 	if err != nil {
 		return "", err
@@ -171,7 +171,7 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
 
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
-	response, err := s.apiClient.ImageBuild(ctx, body, buildOptions)
+	response, err := s.apiClient().ImageBuild(ctx, body, buildOptions)
 	if err != nil {
 		return "", err
 	}
@@ -181,13 +181,13 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
 	aux := func(msg jsonmessage.JSONMessage) {
 		var result dockertypes.BuildResult
 		if err := json.Unmarshal(*msg.Aux, &result); err != nil {
-			fmt.Fprintf(os.Stderr, "Failed to parse aux message: %s", err)
+			fmt.Fprintf(s.stderr(), "Failed to parse aux message: %s", err)
 		} else {
 			imageID = result.ID
 		}
 	}
 
-	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, progBuff.Fd(), true, aux)
+	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, progBuff.FD(), true, aux)
 	if err != nil {
 		if jerr, ok := err.(*jsonmessage.JSONError); ok {
 			// If no error code is set, default to 1
@@ -203,7 +203,7 @@ func (s *composeService) doBuildClassicSimpleImage(ctx context.Context, options
 	// daemon isn't running Windows.
 	if response.OSType != "windows" && runtime.GOOS == "windows" {
 		// if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet {
-		fmt.Fprintln(os.Stdout, "SECURITY WARNING: You are building a Docker "+
+		fmt.Fprintln(s.stdout(), "SECURITY WARNING: You are building a Docker "+
 			"image from Windows against a non-Windows Docker host. All files and "+
 			"directories added to build context will have '-rwxr-xr-x' permissions. "+
 			"It is recommended to double check and reset permissions for sensitive "+

+ 30 - 5
pkg/compose/compose.go

@@ -21,13 +21,16 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
 	"strings"
 
 	"github.com/docker/compose/v2/pkg/api"
 	"github.com/pkg/errors"
 
 	"github.com/compose-spec/compose-go/types"
+	"github.com/docker/cli/cli/command"
 	"github.com/docker/cli/cli/config/configfile"
+	"github.com/docker/cli/cli/streams"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/client"
 	"github.com/sanathkr/go-yaml"
@@ -37,19 +40,41 @@ import (
 var Separator = "-"
 
 // NewComposeService create a local implementation of the compose.Service API
-func NewComposeService(apiClient client.APIClient, configFile *configfile.ConfigFile) api.Service {
+func NewComposeService(dockerCli command.Cli) api.Service {
 	return &composeService{
-		apiClient:  apiClient,
-		configFile: configFile,
+		dockerCli: dockerCli,
 	}
 }
 
 type composeService struct {
-	apiClient  client.APIClient
-	configFile *configfile.ConfigFile
+	dockerCli command.Cli
+}
+
+func (s *composeService) apiClient() client.APIClient {
+	return s.dockerCli.Client()
+}
+
+func (s *composeService) configFile() *configfile.ConfigFile {
+	return s.dockerCli.ConfigFile()
+}
+
+func (s *composeService) stdout() *streams.Out {
+	return s.dockerCli.Out()
+}
+
+func (s *composeService) stdin() *streams.In {
+	return s.dockerCli.In()
+}
+
+func (s *composeService) stderr() io.Writer {
+	return s.dockerCli.Err()
 }
 
 func getCanonicalContainerName(c moby.Container) string {
+	if len(c.Names) == 0 {
+		// corner case, sometime happens on removal. return short ID as a safeguard value
+		return c.ID[:12]
+	}
 	// Names return container canonical name /foo  + link aliases /linked_by/foo
 	for _, name := range c.Names {
 		if strings.LastIndex(name, "/") == 0 {

+ 1 - 1
pkg/compose/containers.go

@@ -52,7 +52,7 @@ func (s *composeService) getContainers(ctx context.Context, project string, oneO
 		f = append(f, oneOffFilter(false))
 	case oneOffInclude:
 	}
-	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(f...),
 		All:     stopped,
 	})

+ 14 - 14
pkg/compose/convergence.go

@@ -180,11 +180,11 @@ func (c *convergence) ensureService(ctx context.Context, project *types.Project,
 			// Scale Down
 			container := container
 			eg.Go(func() error {
-				err := c.service.apiClient.ContainerStop(ctx, container.ID, timeout)
+				err := c.service.apiClient().ContainerStop(ctx, container.ID, timeout)
 				if err != nil {
 					return err
 				}
-				return c.service.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
+				return c.service.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{})
 			})
 			continue
 		}
@@ -395,13 +395,13 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
 	var created moby.Container
 	w := progress.ContextWriter(ctx)
 	w.Event(progress.NewEvent(getContainerProgressName(replaced), progress.Working, "Recreate"))
-	err := s.apiClient.ContainerStop(ctx, replaced.ID, timeout)
+	err := s.apiClient().ContainerStop(ctx, replaced.ID, timeout)
 	if err != nil {
 		return created, err
 	}
 	name := getCanonicalContainerName(replaced)
 	tmpName := fmt.Sprintf("%s_%s", replaced.ID[:12], name)
-	err = s.apiClient.ContainerRename(ctx, replaced.ID, tmpName)
+	err = s.apiClient().ContainerRename(ctx, replaced.ID, tmpName)
 	if err != nil {
 		return created, err
 	}
@@ -419,7 +419,7 @@ func (s *composeService) recreateContainer(ctx context.Context, project *types.P
 	if err != nil {
 		return created, err
 	}
-	err = s.apiClient.ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})
+	err = s.apiClient().ContainerRemove(ctx, replaced.ID, moby.ContainerRemoveOptions{})
 	if err != nil {
 		return created, err
 	}
@@ -444,7 +444,7 @@ func setDependentLifecycle(project *types.Project, service string, strategy stri
 func (s *composeService) startContainer(ctx context.Context, container moby.Container) error {
 	w := progress.ContextWriter(ctx)
 	w.Event(progress.NewEvent(getContainerProgressName(container), progress.Working, "Restart"))
-	err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
+	err := s.apiClient().ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
 	if err != nil {
 		return err
 	}
@@ -468,11 +468,11 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
 		}
 		plat = &p
 	}
-	response, err := s.apiClient.ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
+	response, err := s.apiClient().ContainerCreate(ctx, containerConfig, hostConfig, networkingConfig, plat, name)
 	if err != nil {
 		return created, err
 	}
-	inspectedContainer, err := s.apiClient.ContainerInspect(ctx, response.ID)
+	inspectedContainer, err := s.apiClient().ContainerInspect(ctx, response.ID)
 	if err != nil {
 		return created, err
 	}
@@ -502,7 +502,7 @@ func (s *composeService) createMobyContainer(ctx context.Context, project *types
 			if shortIDAliasExists(created.ID, val.Aliases...) {
 				continue
 			}
-			err = s.apiClient.NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
+			err = s.apiClient().NetworkDisconnect(ctx, netwrk.Name, created.ID, false)
 			if err != nil {
 				return created, err
 			}
@@ -596,7 +596,7 @@ func (s *composeService) connectContainerToNetwork(ctx context.Context, id strin
 			IPv6Address: ipv6Address,
 		}
 	}
-	err := s.apiClient.NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
+	err := s.apiClient().NetworkConnect(ctx, netwrk, id, &network.EndpointSettings{
 		Aliases:           aliases,
 		IPAddress:         ipv4Address,
 		GlobalIPv6Address: ipv6Address,
@@ -619,7 +619,7 @@ func (s *composeService) isServiceHealthy(ctx context.Context, project *types.Pr
 		return false, nil
 	}
 	for _, c := range containers {
-		container, err := s.apiClient.ContainerInspect(ctx, c.ID)
+		container, err := s.apiClient().ContainerInspect(ctx, c.ID)
 		if err != nil {
 			return false, err
 		}
@@ -651,7 +651,7 @@ func (s *composeService) isServiceCompleted(ctx context.Context, project *types.
 		return false, 0, err
 	}
 	for _, c := range containers {
-		container, err := s.apiClient.ContainerInspect(ctx, c.ID)
+		container, err := s.apiClient().ContainerInspect(ctx, c.ID)
 		if err != nil {
 			return false, 0, err
 		}
@@ -671,7 +671,7 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
 	if err != nil {
 		return err
 	}
-	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(
 			projectFilter(project.Name),
 			serviceFilter(service.Name),
@@ -700,7 +700,7 @@ func (s *composeService) startService(ctx context.Context, project *types.Projec
 		eg.Go(func() error {
 			eventName := getContainerProgressName(container)
 			w.Event(progress.StartingEvent(eventName))
-			err := s.apiClient.ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
+			err := s.apiClient().ContainerStart(ctx, container.ID, moby.ContainerStartOptions{})
 			if err == nil {
 				w.Event(progress.StartedEvent(eventName))
 			}

+ 21 - 7
pkg/compose/convergence_test.go

@@ -74,8 +74,11 @@ func TestServiceLinks(t *testing.T) {
 	t.Run("service links default", func(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		defer mockCtrl.Finish()
+
 		apiClient := mocks.NewMockAPIClient(mockCtrl)
-		tested.apiClient = apiClient
+		cli := mocks.NewMockCli(mockCtrl)
+		tested.dockerCli = cli
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 		s.Links = []string{"db"}
 
@@ -95,7 +98,9 @@ func TestServiceLinks(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		defer mockCtrl.Finish()
 		apiClient := mocks.NewMockAPIClient(mockCtrl)
-		tested.apiClient = apiClient
+		cli := mocks.NewMockCli(mockCtrl)
+		tested.dockerCli = cli
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 		s.Links = []string{"db:db"}
 
@@ -115,7 +120,9 @@ func TestServiceLinks(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		defer mockCtrl.Finish()
 		apiClient := mocks.NewMockAPIClient(mockCtrl)
-		tested.apiClient = apiClient
+		cli := mocks.NewMockCli(mockCtrl)
+		tested.dockerCli = cli
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 		s.Links = []string{"db:dbname"}
 
@@ -135,7 +142,9 @@ func TestServiceLinks(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		defer mockCtrl.Finish()
 		apiClient := mocks.NewMockAPIClient(mockCtrl)
-		tested.apiClient = apiClient
+		cli := mocks.NewMockCli(mockCtrl)
+		tested.dockerCli = cli
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 		s.Links = []string{"db:dbname"}
 		s.ExternalLinks = []string{"db1:db2"}
@@ -159,7 +168,9 @@ func TestServiceLinks(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		defer mockCtrl.Finish()
 		apiClient := mocks.NewMockAPIClient(mockCtrl)
-		tested.apiClient = apiClient
+		cli := mocks.NewMockCli(mockCtrl)
+		tested.dockerCli = cli
+		cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 		s.Links = []string{}
 		s.ExternalLinks = []string{}
@@ -189,8 +200,11 @@ func TestServiceLinks(t *testing.T) {
 func TestWaitDependencies(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
-	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+
+	apiClient := mocks.NewMockAPIClient(mockCtrl)
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(apiClient).AnyTimes()
 
 	t.Run("should skip dependencies with scale 0", func(t *testing.T) {
 		dbService := types.ServiceConfig{Name: "db", Scale: 0}

+ 7 - 7
pkg/compose/cp.go

@@ -107,7 +107,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
 
 	// Prepare destination copy info by stat-ing the container path.
 	dstInfo := archive.CopyInfo{Path: dstPath}
-	dstStat, err := s.apiClient.ContainerStatPath(ctx, containerID, dstPath)
+	dstStat, err := s.apiClient().ContainerStatPath(ctx, containerID, dstPath)
 
 	// If the destination is a symbolic link, we should evaluate it.
 	if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
@@ -119,7 +119,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
 		}
 
 		dstInfo.Path = linkTarget
-		dstStat, err = s.apiClient.ContainerStatPath(ctx, containerID, linkTarget)
+		dstStat, err = s.apiClient().ContainerStatPath(ctx, containerID, linkTarget)
 	}
 
 	// Validate the destination path
@@ -143,7 +143,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
 	)
 
 	if srcPath == "-" {
-		content = os.Stdin
+		content = s.stdin()
 		resolvedDstPath = dstInfo.Path
 		if !dstInfo.IsDir {
 			return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
@@ -187,7 +187,7 @@ func (s *composeService) copyToContainer(ctx context.Context, containerID string
 		AllowOverwriteDirWithFile: false,
 		CopyUIDGID:                opts.CopyUIDGID,
 	}
-	return s.apiClient.CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
+	return s.apiClient().CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
 }
 
 func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {
@@ -207,7 +207,7 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
 	// if client requests to follow symbol link, then must decide target file to be copied
 	var rebaseName string
 	if opts.FollowLink {
-		srcStat, err := s.apiClient.ContainerStatPath(ctx, containerID, srcPath)
+		srcStat, err := s.apiClient().ContainerStatPath(ctx, containerID, srcPath)
 
 		// If the destination is a symbolic link, we should follow it.
 		if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
@@ -223,14 +223,14 @@ func (s *composeService) copyFromContainer(ctx context.Context, containerID, src
 		}
 	}
 
-	content, stat, err := s.apiClient.CopyFromContainer(ctx, containerID, srcPath)
+	content, stat, err := s.apiClient().CopyFromContainer(ctx, containerID, srcPath)
 	if err != nil {
 		return err
 	}
 	defer content.Close() //nolint:errcheck
 
 	if dstPath == "-" {
-		_, err = io.Copy(os.Stdout, content)
+		_, err = io.Copy(s.stdout(), content)
 		return err
 	}
 

+ 7 - 7
pkg/compose/create.go

@@ -255,7 +255,7 @@ func (s *composeService) getCreateOptions(ctx context.Context, p *types.Project,
 		return nil, nil, nil, err
 	}
 
-	proxyConfig := types.MappingWithEquals(s.configFile.ParseProxyConfig(s.apiClient.DaemonHost(), nil))
+	proxyConfig := types.MappingWithEquals(s.configFile().ParseProxyConfig(s.apiClient().DaemonHost(), nil))
 	env := proxyConfig.OverrideBy(service.Environment)
 
 	containerConfig := container.Config{
@@ -694,7 +694,7 @@ func (s *composeService) buildContainerVolumes(ctx context.Context, p types.Proj
 	var mounts = []mount.Mount{}
 
 	image := getImageName(service, p.Name)
-	imgInspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, image)
+	imgInspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, image)
 	if err != nil {
 		return nil, nil, nil, err
 	}
@@ -1008,7 +1008,7 @@ func getAliases(s types.ServiceConfig, c *types.ServiceNetworkConfig) []string {
 }
 
 func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfig) error {
-	_, err := s.apiClient.NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
+	_, err := s.apiClient().NetworkInspect(ctx, n.Name, moby.NetworkInspectOptions{})
 	if err != nil {
 		if errdefs.IsNotFound(err) {
 			if n.External.External {
@@ -1066,7 +1066,7 @@ func (s *composeService) ensureNetwork(ctx context.Context, n types.NetworkConfi
 			networkEventName := fmt.Sprintf("Network %s", n.Name)
 			w := progress.ContextWriter(ctx)
 			w.Event(progress.CreatingEvent(networkEventName))
-			if _, err := s.apiClient.NetworkCreate(ctx, n.Name, createOpts); err != nil {
+			if _, err := s.apiClient().NetworkCreate(ctx, n.Name, createOpts); err != nil {
 				w.Event(progress.ErrorEvent(networkEventName))
 				return errors.Wrapf(err, "failed to create network %s", n.Name)
 			}
@@ -1083,7 +1083,7 @@ func (s *composeService) removeNetwork(ctx context.Context, networkID string, ne
 	eventName := fmt.Sprintf("Network %s", networkName)
 	w.Event(progress.RemovingEvent(eventName))
 
-	if err := s.apiClient.NetworkRemove(ctx, networkID); err != nil {
+	if err := s.apiClient().NetworkRemove(ctx, networkID); err != nil {
 		w.Event(progress.ErrorEvent(eventName))
 		return errors.Wrapf(err, fmt.Sprintf("failed to remove network %s", networkID))
 	}
@@ -1093,7 +1093,7 @@ func (s *composeService) removeNetwork(ctx context.Context, networkID string, ne
 }
 
 func (s *composeService) ensureVolume(ctx context.Context, volume types.VolumeConfig, project string) error {
-	inspected, err := s.apiClient.VolumeInspect(ctx, volume.Name)
+	inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name)
 	if err != nil {
 		if !errdefs.IsNotFound(err) {
 			return err
@@ -1124,7 +1124,7 @@ func (s *composeService) createVolume(ctx context.Context, volume types.VolumeCo
 	eventName := fmt.Sprintf("Volume %q", volume.Name)
 	w := progress.ContextWriter(ctx)
 	w.Event(progress.CreatingEvent(eventName))
-	_, err := s.apiClient.VolumeCreate(ctx, volume_api.VolumeCreateBody{
+	_, err := s.apiClient().VolumeCreate(ctx, volume_api.VolumeCreateBody{
 		Labels:     volume.Labels,
 		Name:       volume.Name,
 		Driver:     volume.Driver,

+ 6 - 6
pkg/compose/down.go

@@ -127,7 +127,7 @@ func (s *composeService) ensureImagesDown(ctx context.Context, projectName strin
 
 func (s *composeService) ensureNetworksDown(ctx context.Context, projectName string) ([]downOp, error) {
 	var ops []downOp
-	networks, err := s.apiClient.NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))})
+	networks, err := s.apiClient().NetworkList(ctx, moby.NetworkListOptions{Filters: filters.NewArgs(projectFilter(projectName))})
 	if err != nil {
 		return ops, err
 	}
@@ -159,7 +159,7 @@ func (s *composeService) getServiceImages(options api.DownOptions, projectName s
 func (s *composeService) removeImage(ctx context.Context, image string, w progress.Writer) error {
 	id := fmt.Sprintf("Image %s", image)
 	w.Event(progress.NewEvent(id, progress.Working, "Removing"))
-	_, err := s.apiClient.ImageRemove(ctx, image, moby.ImageRemoveOptions{})
+	_, err := s.apiClient().ImageRemove(ctx, image, moby.ImageRemoveOptions{})
 	if err == nil {
 		w.Event(progress.NewEvent(id, progress.Done, "Removed"))
 		return nil
@@ -174,7 +174,7 @@ func (s *composeService) removeImage(ctx context.Context, image string, w progre
 func (s *composeService) removeVolume(ctx context.Context, id string, w progress.Writer) error {
 	resource := fmt.Sprintf("Volume %s", id)
 	w.Event(progress.NewEvent(resource, progress.Working, "Removing"))
-	err := s.apiClient.VolumeRemove(ctx, id, true)
+	err := s.apiClient().VolumeRemove(ctx, id, true)
 	if err == nil {
 		w.Event(progress.NewEvent(resource, progress.Done, "Removed"))
 		return nil
@@ -193,7 +193,7 @@ func (s *composeService) stopContainers(ctx context.Context, w progress.Writer,
 		eg.Go(func() error {
 			eventName := getContainerProgressName(container)
 			w.Event(progress.StoppingEvent(eventName))
-			err := s.apiClient.ContainerStop(ctx, container.ID, timeout)
+			err := s.apiClient().ContainerStop(ctx, container.ID, timeout)
 			if err != nil {
 				w.Event(progress.ErrorMessageEvent(eventName, "Error while Stopping"))
 				return err
@@ -218,7 +218,7 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
 				return err
 			}
 			w.Event(progress.RemovingEvent(eventName))
-			err = s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
+			err = s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
 				Force:         true,
 				RemoveVolumes: volumes,
 			})
@@ -236,7 +236,7 @@ func (s *composeService) removeContainers(ctx context.Context, w progress.Writer
 func (s *composeService) getProjectWithVolumes(ctx context.Context, containers Containers, projectName string) (*types.Project, error) {
 	containers = containers.filter(isNotOneOff)
 	project, _ := s.projectFromName(containers, projectName)
-	volumes, err := s.apiClient.VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
+	volumes, err := s.apiClient().VolumeList(ctx, filters.NewArgs(projectFilter(projectName)))
 	if err != nil {
 		return nil, err
 	}

+ 12 - 3
pkg/compose/down_test.go

@@ -34,8 +34,11 @@ import (
 func TestDown(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
 		[]moby.Container{
@@ -67,8 +70,11 @@ func TestDown(t *testing.T) {
 func TestDownRemoveOrphans(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
 		[]moby.Container{
@@ -99,8 +105,11 @@ func TestDownRemoveOrphans(t *testing.T) {
 func TestDownRemoveVolumes(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(
 		[]moby.Container{testContainer("service1", "123", false)}, nil)

+ 1 - 1
pkg/compose/events.go

@@ -30,7 +30,7 @@ import (
 )
 
 func (s *composeService) Events(ctx context.Context, project string, options api.EventsOptions) error {
-	events, errors := s.apiClient.Events(ctx, moby.EventsOptions{
+	events, errors := s.apiClient().Events(ctx, moby.EventsOptions{
 		Filters: filters.NewArgs(projectFilter(project)),
 	})
 	for {

+ 10 - 11
pkg/compose/exec.go

@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"io"
 
-	"github.com/docker/cli/cli/streams"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/pkg/stdcopy"
@@ -36,7 +35,7 @@ func (s *composeService) Exec(ctx context.Context, project string, opts api.RunO
 		return 0, err
 	}
 
-	exec, err := s.apiClient.ContainerExecCreate(ctx, container.ID, moby.ExecConfig{
+	exec, err := s.apiClient().ContainerExecCreate(ctx, container.ID, moby.ExecConfig{
 		Cmd:        opts.Command,
 		Env:        opts.Environment,
 		User:       opts.User,
@@ -54,13 +53,13 @@ func (s *composeService) Exec(ctx context.Context, project string, opts api.RunO
 	}
 
 	if opts.Detach {
-		return 0, s.apiClient.ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{
+		return 0, s.apiClient().ContainerExecStart(ctx, exec.ID, moby.ExecStartCheck{
 			Detach: true,
 			Tty:    opts.Tty,
 		})
 	}
 
-	resp, err := s.apiClient.ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{
+	resp, err := s.apiClient().ContainerExecAttach(ctx, exec.ID, moby.ExecStartCheck{
 		Tty: opts.Tty,
 	})
 	if err != nil {
@@ -69,7 +68,7 @@ func (s *composeService) Exec(ctx context.Context, project string, opts api.RunO
 	defer resp.Close() //nolint:errcheck
 
 	if opts.Tty {
-		s.monitorTTySize(ctx, exec.ID, s.apiClient.ContainerExecResize)
+		s.monitorTTySize(ctx, exec.ID, s.apiClient().ContainerExecResize)
 		if err != nil {
 			return 0, err
 		}
@@ -90,12 +89,12 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
 
 	stdout := ContainerStdout{HijackedResponse: resp}
 	stdin := ContainerStdin{HijackedResponse: resp}
-	r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
+	r, err := s.getEscapeKeyProxy(s.stdin(), opts.Tty)
 	if err != nil {
 		return err
 	}
 
-	in := streams.NewIn(opts.Stdin)
+	in := s.stdin()
 	if in.IsTerminal() && opts.Tty {
 		state, err := term.SetRawTerminal(in.FD())
 		if err != nil {
@@ -106,10 +105,10 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
 
 	go func() {
 		if opts.Tty {
-			_, err := io.Copy(opts.Stdout, stdout)
+			_, err := io.Copy(s.stdout(), stdout)
 			outputDone <- err
 		} else {
-			_, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout)
+			_, err := stdcopy.StdCopy(s.stdout(), s.stderr(), stdout)
 			outputDone <- err
 		}
 		stdout.Close() //nolint:errcheck
@@ -140,7 +139,7 @@ func (s *composeService) interactiveExec(ctx context.Context, opts api.RunOption
 }
 
 func (s *composeService) getExecTarget(ctx context.Context, projectName string, opts api.RunOptions) (moby.Container, error) {
-	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	containers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(
 			projectFilter(projectName),
 			serviceFilter(opts.Service),
@@ -158,7 +157,7 @@ func (s *composeService) getExecTarget(ctx context.Context, projectName string,
 }
 
 func (s *composeService) getExecExitStatus(ctx context.Context, execID string) (int, error) {
-	resp, err := s.apiClient.ContainerExecInspect(ctx, execID)
+	resp, err := s.apiClient().ContainerExecInspect(ctx, execID)
 	if err != nil {
 		return 0, err
 	}

+ 2 - 2
pkg/compose/images.go

@@ -32,7 +32,7 @@ import (
 )
 
 func (s *composeService) Images(ctx context.Context, projectName string, options api.ImagesOptions) ([]api.ImageSummary, error) {
-	allContainers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	allContainers, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
 		All:     true,
 		Filters: filters.NewArgs(projectFilter(projectName)),
 	})
@@ -83,7 +83,7 @@ func (s *composeService) getImages(ctx context.Context, images []string) (map[st
 	for _, img := range images {
 		img := img
 		eg.Go(func() error {
-			inspect, _, err := s.apiClient.ImageInspectWithRaw(ctx, img)
+			inspect, _, err := s.apiClient().ImageInspectWithRaw(ctx, img)
 			if err != nil {
 				if errdefs.IsNotFound(err) {
 					return nil

+ 1 - 1
pkg/compose/kill.go

@@ -54,7 +54,7 @@ func (s *composeService) kill(ctx context.Context, project *types.Project, optio
 			eg.Go(func() error {
 				eventName := getContainerProgressName(container)
 				w.Event(progress.KillingEvent(eventName))
-				err := s.apiClient.ContainerKill(ctx, container.ID, options.Signal)
+				err := s.apiClient().ContainerKill(ctx, container.ID, options.Signal)
 				if err != nil {
 					w.Event(progress.ErrorMessageEvent(eventName, "Error while Killing"))
 					return err

+ 8 - 2
pkg/compose/kill_test.go

@@ -39,8 +39,11 @@ var tested = composeService{}
 func TestKillAll(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{testService("service1"), testService("service2")}}
 
@@ -61,8 +64,11 @@ func TestKillSignal(t *testing.T) {
 	const serviceName = "service1"
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	project := types.Project{Name: strings.ToLower(testProject), Services: []types.ServiceConfig{testService(serviceName)}}
 	listOptions := moby.ContainerListOptions{

+ 2 - 2
pkg/compose/logs.go

@@ -75,13 +75,13 @@ func (s *composeService) Logs(ctx context.Context, projectName string, consumer
 }
 
 func (s *composeService) logContainers(ctx context.Context, consumer api.LogConsumer, c types.Container, options api.LogOptions) error {
-	cnt, err := s.apiClient.ContainerInspect(ctx, c.ID)
+	cnt, err := s.apiClient().ContainerInspect(ctx, c.ID)
 	if err != nil {
 		return err
 	}
 
 	service := c.Labels[api.ServiceLabel]
-	r, err := s.apiClient.ContainerLogs(ctx, cnt.ID, types.ContainerLogsOptions{
+	r, err := s.apiClient().ContainerLogs(ctx, cnt.ID, types.ContainerLogsOptions{
 		ShowStdout: true,
 		ShowStderr: true,
 		Follow:     options.Follow,

+ 1 - 1
pkg/compose/ls.go

@@ -30,7 +30,7 @@ import (
 )
 
 func (s *composeService) List(ctx context.Context, opts api.ListOptions) ([]api.Stack, error) {
-	list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	list, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(hasProjectLabelFilter()),
 		All:     opts.All,
 	})

+ 2 - 2
pkg/compose/pause.go

@@ -42,7 +42,7 @@ func (s *composeService) pause(ctx context.Context, project string, options api.
 	eg, ctx := errgroup.WithContext(ctx)
 	containers.forEach(func(container moby.Container) {
 		eg.Go(func() error {
-			err := s.apiClient.ContainerPause(ctx, container.ID)
+			err := s.apiClient().ContainerPause(ctx, container.ID)
 			if err == nil {
 				eventName := getContainerProgressName(container)
 				w.Event(progress.NewEvent(eventName, progress.Done, "Paused"))
@@ -70,7 +70,7 @@ func (s *composeService) unPause(ctx context.Context, project string, options ap
 	eg, ctx := errgroup.WithContext(ctx)
 	containers.forEach(func(container moby.Container) {
 		eg.Go(func() error {
-			err = s.apiClient.ContainerUnpause(ctx, container.ID)
+			err = s.apiClient().ContainerUnpause(ctx, container.ID)
 			if err == nil {
 				eventName := getContainerProgressName(container)
 				w.Event(progress.NewEvent(eventName, progress.Done, "Unpaused"))

+ 1 - 1
pkg/compose/port.go

@@ -27,7 +27,7 @@ import (
 )
 
 func (s *composeService) Port(ctx context.Context, project string, service string, port int, options api.PortOptions) (string, int, error) {
-	list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+	list, err := s.apiClient().ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(
 			projectFilter(project),
 			serviceFilter(service),

+ 1 - 1
pkg/compose/ps.go

@@ -53,7 +53,7 @@ func (s *composeService) Ps(ctx context.Context, projectName string, options api
 				})
 			}
 
-			inspect, err := s.apiClient.ContainerInspect(ctx, container.ID)
+			inspect, err := s.apiClient().ContainerInspect(ctx, container.ID)
 			if err != nil {
 				return err
 			}

+ 4 - 1
pkg/compose/ps_test.go

@@ -34,8 +34,11 @@ import (
 func TestPs(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	ctx := context.Background()
 	args := filters.NewArgs(projectFilter(strings.ToLower(testProject)))

+ 5 - 5
pkg/compose/pull.go

@@ -46,7 +46,7 @@ func (s *composeService) Pull(ctx context.Context, project *types.Project, opts
 }
 
 func (s *composeService) pull(ctx context.Context, project *types.Project, opts api.PullOptions) error {
-	info, err := s.apiClient.Info(ctx)
+	info, err := s.apiClient().Info(ctx)
 	if err != nil {
 		return err
 	}
@@ -70,7 +70,7 @@ func (s *composeService) pull(ctx context.Context, project *types.Project, opts
 			continue
 		}
 		eg.Go(func() error {
-			err := s.pullServiceImage(ctx, service, info, s.configFile, w, false)
+			err := s.pullServiceImage(ctx, service, info, s.configFile(), w, false)
 			if err != nil {
 				if !opts.IgnoreFailures {
 					if service.Build != nil {
@@ -124,7 +124,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
 		return err
 	}
 
-	stream, err := s.apiClient.ImagePull(ctx, service.Image, moby.ImagePullOptions{
+	stream, err := s.apiClient().ImagePull(ctx, service.Image, moby.ImagePullOptions{
 		RegistryAuth: base64.URLEncoding.EncodeToString(buf),
 		Platform:     service.Platform,
 	})
@@ -162,7 +162,7 @@ func (s *composeService) pullServiceImage(ctx context.Context, service types.Ser
 }
 
 func (s *composeService) pullRequiredImages(ctx context.Context, project *types.Project, images map[string]string, quietPull bool) error {
-	info, err := s.apiClient.Info(ctx)
+	info, err := s.apiClient().Info(ctx)
 	if err != nil {
 		return err
 	}
@@ -198,7 +198,7 @@ func (s *composeService) pullRequiredImages(ctx context.Context, project *types.
 		for _, service := range needPull {
 			service := service
 			eg.Go(func() error {
-				err := s.pullServiceImage(ctx, service, info, s.configFile, w, quietPull)
+				err := s.pullServiceImage(ctx, service, info, s.configFile(), w, quietPull)
 				if err != nil && service.Build != nil {
 					// image can be built, so we can ignore pull failure
 					return nil

+ 3 - 3
pkg/compose/push.go

@@ -45,7 +45,7 @@ func (s *composeService) Push(ctx context.Context, project *types.Project, optio
 func (s *composeService) push(ctx context.Context, project *types.Project, options api.PushOptions) error {
 	eg, ctx := errgroup.WithContext(ctx)
 
-	info, err := s.apiClient.Info(ctx)
+	info, err := s.apiClient().Info(ctx)
 	if err != nil {
 		return err
 	}
@@ -65,7 +65,7 @@ func (s *composeService) push(ctx context.Context, project *types.Project, optio
 		}
 		service := service
 		eg.Go(func() error {
-			err := s.pushServiceImage(ctx, service, info, s.configFile, w)
+			err := s.pushServiceImage(ctx, service, info, s.configFile(), w)
 			if err != nil {
 				if !options.IgnoreFailures {
 					return err
@@ -103,7 +103,7 @@ func (s *composeService) pushServiceImage(ctx context.Context, service types.Ser
 		return err
 	}
 
-	stream, err := s.apiClient.ImagePush(ctx, service.Image, moby.ImagePushOptions{
+	stream, err := s.apiClient().ImagePush(ctx, service.Image, moby.ImagePushOptions{
 		RegistryAuth: base64.URLEncoding.EncodeToString(buf),
 	})
 	if err != nil {

+ 1 - 1
pkg/compose/remove.go

@@ -73,7 +73,7 @@ func (s *composeService) remove(ctx context.Context, containers Containers, opti
 		eg.Go(func() error {
 			eventName := getContainerProgressName(container)
 			w.Event(progress.RemovingEvent(eventName))
-			err := s.apiClient.ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
+			err := s.apiClient().ContainerRemove(ctx, container.ID, moby.ContainerRemoveOptions{
 				RemoveVolumes: options.Volumes,
 				Force:         options.Force,
 			})

+ 1 - 1
pkg/compose/restart.go

@@ -59,7 +59,7 @@ func (s *composeService) restart(ctx context.Context, projectName string, option
 			eg.Go(func() error {
 				eventName := getContainerProgressName(container)
 				w.Event(progress.RestartingEvent(eventName))
-				err := s.apiClient.ContainerRestart(ctx, container.ID, options.Timeout)
+				err := s.apiClient().ContainerRestart(ctx, container.ID, options.Timeout)
 				if err == nil {
 					w.Event(progress.StartedEvent(eventName))
 				}

+ 12 - 13
pkg/compose/run.go

@@ -22,7 +22,6 @@ import (
 	"io"
 
 	"github.com/compose-spec/compose-go/types"
-	"github.com/docker/cli/cli/streams"
 	"github.com/docker/compose/v2/pkg/api"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/container"
@@ -39,11 +38,11 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
 	}
 
 	if opts.Detach {
-		err := s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
+		err := s.apiClient().ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
 		if err != nil {
 			return 0, err
 		}
-		fmt.Fprintln(opts.Stdout, containerID)
+		fmt.Fprintln(s.stdout(), containerID)
 		return 0, nil
 	}
 
@@ -51,7 +50,8 @@ func (s *composeService) RunOneOffContainer(ctx context.Context, project *types.
 }
 
 func (s *composeService) runInteractive(ctx context.Context, containerID string, opts api.RunOptions) (int, error) {
-	r, err := s.getEscapeKeyProxy(opts.Stdin, opts.Tty)
+	in := s.stdin()
+	r, err := s.getEscapeKeyProxy(in, opts.Tty)
 	if err != nil {
 		return 0, err
 	}
@@ -61,7 +61,6 @@ func (s *composeService) runInteractive(ctx context.Context, containerID string,
 		return 0, err
 	}
 
-	in := streams.NewIn(opts.Stdin)
 	if in.IsTerminal() && opts.Tty {
 		state, err := term.SetRawTerminal(in.FD())
 		if err != nil {
@@ -75,10 +74,10 @@ func (s *composeService) runInteractive(ctx context.Context, containerID string,
 
 	go func() {
 		if opts.Tty {
-			_, err := io.Copy(opts.Stdout, stdout) //nolint:errcheck
+			_, err := io.Copy(s.stdout(), stdout) //nolint:errcheck
 			outputDone <- err
 		} else {
-			_, err := stdcopy.StdCopy(opts.Stdout, opts.Stderr, stdout) //nolint:errcheck
+			_, err := stdcopy.StdCopy(s.stdout(), s.stderr(), stdout) //nolint:errcheck
 			outputDone <- err
 		}
 		stdout.Close() //nolint:errcheck
@@ -90,12 +89,12 @@ func (s *composeService) runInteractive(ctx context.Context, containerID string,
 		stdin.Close() //nolint:errcheck
 	}()
 
-	err = s.apiClient.ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
+	err = s.apiClient().ContainerStart(ctx, containerID, moby.ContainerStartOptions{})
 	if err != nil {
 		return 0, err
 	}
 
-	s.monitorTTySize(ctx, containerID, s.apiClient.ContainerResize)
+	s.monitorTTySize(ctx, containerID, s.apiClient().ContainerResize)
 
 	for {
 		select {
@@ -119,7 +118,7 @@ func (s *composeService) runInteractive(ctx context.Context, containerID string,
 }
 
 func (s *composeService) terminateRun(ctx context.Context, containerID string, opts api.RunOptions) (exitCode int, err error) {
-	exitCh, errCh := s.apiClient.ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
+	exitCh, errCh := s.apiClient().ContainerWait(ctx, containerID, container.WaitConditionNotRunning)
 	select {
 	case exit := <-exitCh:
 		exitCode = int(exit.StatusCode)
@@ -127,7 +126,7 @@ func (s *composeService) terminateRun(ctx context.Context, containerID string, o
 		return
 	}
 	if opts.AutoRemove {
-		err = s.apiClient.ContainerRemove(ctx, containerID, moby.ContainerRemoveOptions{})
+		err = s.apiClient().ContainerRemove(ctx, containerID, moby.ContainerRemoveOptions{})
 	}
 	return
 }
@@ -185,8 +184,8 @@ func (s *composeService) getEscapeKeyProxy(r io.ReadCloser, isTty bool) (io.Read
 		return r, nil
 	}
 	var escapeKeys = []byte{16, 17}
-	if s.configFile.DetachKeys != "" {
-		customEscapeKeys, err := term.ToBytes(s.configFile.DetachKeys)
+	if s.configFile().DetachKeys != "" {
+		customEscapeKeys, err := term.ToBytes(s.configFile().DetachKeys)
 		if err != nil {
 			return nil, err
 		}

+ 1 - 1
pkg/compose/start.go

@@ -107,7 +107,7 @@ func (s *composeService) watchContainers(ctx context.Context, projectName string
 				return nil
 			}
 
-			inspected, err := s.apiClient.ContainerInspect(ctx, event.Container)
+			inspected, err := s.apiClient().ContainerInspect(ctx, event.Container)
 			if err != nil {
 				return err
 			}

+ 4 - 1
pkg/compose/stop_test.go

@@ -33,8 +33,11 @@ import (
 func TestStopTimeout(t *testing.T) {
 	mockCtrl := gomock.NewController(t)
 	defer mockCtrl.Finish()
+
 	api := mocks.NewMockAPIClient(mockCtrl)
-	tested.apiClient = api
+	cli := mocks.NewMockCli(mockCtrl)
+	tested.dockerCli = cli
+	cli.EXPECT().Client().Return(api).AnyTimes()
 
 	ctx := context.Background()
 	api.EXPECT().ContainerList(gomock.Any(), projectFilterListOpt()).Return(

+ 1 - 1
pkg/compose/top.go

@@ -37,7 +37,7 @@ func (s *composeService) Top(ctx context.Context, projectName string, services [
 	for i, container := range containers {
 		i, container := i, container
 		eg.Go(func() error {
-			topContent, err := s.apiClient.ContainerTop(ctx, container.ID, []string{})
+			topContent, err := s.apiClient().ContainerTop(ctx, container.ID, []string{})
 			if err != nil {
 				return err
 			}

+ 301 - 0
pkg/mocks/mock_docker_cli.go

@@ -0,0 +1,301 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/docker/cli/cli/command (interfaces: Cli)
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	io "io"
+	reflect "reflect"
+
+	command "github.com/docker/cli/cli/command"
+	configfile "github.com/docker/cli/cli/config/configfile"
+	docker "github.com/docker/cli/cli/context/docker"
+	store "github.com/docker/cli/cli/context/store"
+	store0 "github.com/docker/cli/cli/manifest/store"
+	client "github.com/docker/cli/cli/registry/client"
+	streams "github.com/docker/cli/cli/streams"
+	trust "github.com/docker/cli/cli/trust"
+	client0 "github.com/docker/docker/client"
+	gomock "github.com/golang/mock/gomock"
+	client1 "github.com/theupdateframework/notary/client"
+)
+
+// MockCli is a mock of Cli interface.
+type MockCli struct {
+	ctrl     *gomock.Controller
+	recorder *MockCliMockRecorder
+}
+
+// MockCliMockRecorder is the mock recorder for MockCli.
+type MockCliMockRecorder struct {
+	mock *MockCli
+}
+
+// NewMockCli creates a new mock instance.
+func NewMockCli(ctrl *gomock.Controller) *MockCli {
+	mock := &MockCli{ctrl: ctrl}
+	mock.recorder = &MockCliMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCli) EXPECT() *MockCliMockRecorder {
+	return m.recorder
+}
+
+// Apply mocks base method.
+func (m *MockCli) Apply(arg0 ...command.DockerCliOption) error {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{}
+	for _, a := range arg0 {
+		varargs = append(varargs, a)
+	}
+	ret := m.ctrl.Call(m, "Apply", varargs...)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// Apply indicates an expected call of Apply.
+func (mr *MockCliMockRecorder) Apply(arg0 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Apply", reflect.TypeOf((*MockCli)(nil).Apply), arg0...)
+}
+
+// Client mocks base method.
+func (m *MockCli) Client() client0.APIClient {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Client")
+	ret0, _ := ret[0].(client0.APIClient)
+	return ret0
+}
+
+// Client indicates an expected call of Client.
+func (mr *MockCliMockRecorder) Client() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Client", reflect.TypeOf((*MockCli)(nil).Client))
+}
+
+// ClientInfo mocks base method.
+func (m *MockCli) ClientInfo() command.ClientInfo {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ClientInfo")
+	ret0, _ := ret[0].(command.ClientInfo)
+	return ret0
+}
+
+// ClientInfo indicates an expected call of ClientInfo.
+func (mr *MockCliMockRecorder) ClientInfo() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientInfo", reflect.TypeOf((*MockCli)(nil).ClientInfo))
+}
+
+// ConfigFile mocks base method.
+func (m *MockCli) ConfigFile() *configfile.ConfigFile {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ConfigFile")
+	ret0, _ := ret[0].(*configfile.ConfigFile)
+	return ret0
+}
+
+// ConfigFile indicates an expected call of ConfigFile.
+func (mr *MockCliMockRecorder) ConfigFile() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigFile", reflect.TypeOf((*MockCli)(nil).ConfigFile))
+}
+
+// ContentTrustEnabled mocks base method.
+func (m *MockCli) ContentTrustEnabled() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ContentTrustEnabled")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// ContentTrustEnabled indicates an expected call of ContentTrustEnabled.
+func (mr *MockCliMockRecorder) ContentTrustEnabled() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContentTrustEnabled", reflect.TypeOf((*MockCli)(nil).ContentTrustEnabled))
+}
+
+// ContextStore mocks base method.
+func (m *MockCli) ContextStore() store.Store {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ContextStore")
+	ret0, _ := ret[0].(store.Store)
+	return ret0
+}
+
+// ContextStore indicates an expected call of ContextStore.
+func (mr *MockCliMockRecorder) ContextStore() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ContextStore", reflect.TypeOf((*MockCli)(nil).ContextStore))
+}
+
+// CurrentContext mocks base method.
+func (m *MockCli) CurrentContext() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "CurrentContext")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// CurrentContext indicates an expected call of CurrentContext.
+func (mr *MockCliMockRecorder) CurrentContext() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CurrentContext", reflect.TypeOf((*MockCli)(nil).CurrentContext))
+}
+
+// DefaultVersion mocks base method.
+func (m *MockCli) DefaultVersion() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DefaultVersion")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// DefaultVersion indicates an expected call of DefaultVersion.
+func (mr *MockCliMockRecorder) DefaultVersion() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultVersion", reflect.TypeOf((*MockCli)(nil).DefaultVersion))
+}
+
+// DockerEndpoint mocks base method.
+func (m *MockCli) DockerEndpoint() docker.Endpoint {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DockerEndpoint")
+	ret0, _ := ret[0].(docker.Endpoint)
+	return ret0
+}
+
+// DockerEndpoint indicates an expected call of DockerEndpoint.
+func (mr *MockCliMockRecorder) DockerEndpoint() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DockerEndpoint", reflect.TypeOf((*MockCli)(nil).DockerEndpoint))
+}
+
+// Err mocks base method.
+func (m *MockCli) Err() io.Writer {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Err")
+	ret0, _ := ret[0].(io.Writer)
+	return ret0
+}
+
+// Err indicates an expected call of Err.
+func (mr *MockCliMockRecorder) Err() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockCli)(nil).Err))
+}
+
+// In mocks base method.
+func (m *MockCli) In() *streams.In {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "In")
+	ret0, _ := ret[0].(*streams.In)
+	return ret0
+}
+
+// In indicates an expected call of In.
+func (mr *MockCliMockRecorder) In() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "In", reflect.TypeOf((*MockCli)(nil).In))
+}
+
+// ManifestStore mocks base method.
+func (m *MockCli) ManifestStore() store0.Store {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ManifestStore")
+	ret0, _ := ret[0].(store0.Store)
+	return ret0
+}
+
+// ManifestStore indicates an expected call of ManifestStore.
+func (mr *MockCliMockRecorder) ManifestStore() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ManifestStore", reflect.TypeOf((*MockCli)(nil).ManifestStore))
+}
+
+// NotaryClient mocks base method.
+func (m *MockCli) NotaryClient(arg0 trust.ImageRefAndAuth, arg1 []string) (client1.Repository, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "NotaryClient", arg0, arg1)
+	ret0, _ := ret[0].(client1.Repository)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// NotaryClient indicates an expected call of NotaryClient.
+func (mr *MockCliMockRecorder) NotaryClient(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotaryClient", reflect.TypeOf((*MockCli)(nil).NotaryClient), arg0, arg1)
+}
+
+// Out mocks base method.
+func (m *MockCli) Out() *streams.Out {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Out")
+	ret0, _ := ret[0].(*streams.Out)
+	return ret0
+}
+
+// Out indicates an expected call of Out.
+func (mr *MockCliMockRecorder) Out() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Out", reflect.TypeOf((*MockCli)(nil).Out))
+}
+
+// RegistryClient mocks base method.
+func (m *MockCli) RegistryClient(arg0 bool) client.RegistryClient {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "RegistryClient", arg0)
+	ret0, _ := ret[0].(client.RegistryClient)
+	return ret0
+}
+
+// RegistryClient indicates an expected call of RegistryClient.
+func (mr *MockCliMockRecorder) RegistryClient(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryClient", reflect.TypeOf((*MockCli)(nil).RegistryClient), arg0)
+}
+
+// ServerInfo mocks base method.
+func (m *MockCli) ServerInfo() command.ServerInfo {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ServerInfo")
+	ret0, _ := ret[0].(command.ServerInfo)
+	return ret0
+}
+
+// ServerInfo indicates an expected call of ServerInfo.
+func (mr *MockCliMockRecorder) ServerInfo() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerInfo", reflect.TypeOf((*MockCli)(nil).ServerInfo))
+}
+
+// SetIn mocks base method.
+func (m *MockCli) SetIn(arg0 *streams.In) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetIn", arg0)
+}
+
+// SetIn indicates an expected call of SetIn.
+func (mr *MockCliMockRecorder) SetIn(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIn", reflect.TypeOf((*MockCli)(nil).SetIn), arg0)
+}
+
+// StackOrchestrator mocks base method.
+func (m *MockCli) StackOrchestrator(arg0 string) (command.Orchestrator, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StackOrchestrator", arg0)
+	ret0, _ := ret[0].(command.Orchestrator)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// StackOrchestrator indicates an expected call of StackOrchestrator.
+func (mr *MockCliMockRecorder) StackOrchestrator(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StackOrchestrator", reflect.TypeOf((*MockCli)(nil).StackOrchestrator), arg0)
+}