فهرست منبع

Setup Compose service using functional parameters
This commit introduces WithMaxConcurrency and WithDryRun to replace direct mutators on composeService
commands and flags are translated into a set of functional parameters which are eventually applied
as a ComposeService is created just before being actually used by a command

Signed-off-by: Nicolas De Loof <[email protected]>

Nicolas De Loof 2 ماه پیش
والد
کامیت
7f668bd7fe

+ 4 - 6
cmd/compose/alpha.go

@@ -1,5 +1,4 @@
 /*
-
    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.
@@ -16,12 +15,11 @@ package compose
 
 import (
 	"github.com/docker/cli/cli/command"
-	"github.com/docker/compose/v2/pkg/api"
 	"github.com/spf13/cobra"
 )
 
 // alphaCommand groups all experimental subcommands
-func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	cmd := &cobra.Command{
 		Short:  "Experimental commands",
 		Use:    "alpha [COMMAND]",
@@ -31,9 +29,9 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		},
 	}
 	cmd.AddCommand(
-		vizCommand(p, dockerCli, backend),
-		publishCommand(p, dockerCli, backend),
-		generateCommand(p, backend),
+		vizCommand(p, dockerCli, backendOptions),
+		publishCommand(p, dockerCli, backendOptions),
+		generateCommand(p, dockerCli, backendOptions),
 	)
 	return cmd
 }

+ 8 - 3
cmd/compose/attach.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -35,7 +36,7 @@ type attachOpts struct {
 	proxy      bool
 }
 
-func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func attachCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := attachOpts{
 		composeOptions: &composeOptions{
 			ProjectOptions: p,
@@ -50,7 +51,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runAttach(ctx, dockerCli, backend, opts)
+			return runAttach(ctx, dockerCli, backendOptions, opts)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -63,7 +64,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 	return runCmd
 }
 
-func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts attachOpts) error {
+func runAttach(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts attachOpts) error {
 	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
@@ -76,5 +77,9 @@ func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Compose,
 		NoStdin:    opts.noStdin,
 		Proxy:      opts.proxy,
 	}
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Attach(ctx, projectName, attachOpts)
 }

+ 8 - 3
cmd/compose/build.go

@@ -26,6 +26,7 @@ import (
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
 	cliopts "github.com/docker/cli/opts"
+	"github.com/docker/compose/v2/pkg/compose"
 	ui "github.com/docker/compose/v2/pkg/progress"
 	"github.com/spf13/cobra"
 
@@ -90,7 +91,7 @@ func (opts buildOptions) toAPIBuildOptions(services []string) (api.BuildOptions,
 	}, nil
 }
 
-func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := buildOptions{
 		ProjectOptions: p,
 	}
@@ -115,7 +116,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			if cmd.Flags().Changed("progress") && opts.ssh == "" {
 				fmt.Fprint(os.Stderr, "--progress is a global compose flag, better use `docker compose --progress xx build ...\n")
 			}
-			return runBuild(ctx, dockerCli, backend, opts, args)
+			return runBuild(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -148,7 +149,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return cmd
 }
 
-func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts buildOptions, services []string) error {
+func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error {
 	opts.All = true // do not drop resources as build may involve some dependencies by additional_contexts
 	project, _, err := opts.ToProject(ctx, dockerCli, nil, cli.WithResolvedPaths(true), cli.WithoutEnvironmentResolution)
 	if err != nil {
@@ -165,5 +166,9 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Compose, o
 	}
 	apiBuildOptions.Attestations = true
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Build(ctx, project, apiBuildOptions)
 }

+ 8 - 3
cmd/compose/commit.go

@@ -22,6 +22,7 @@ import (
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/cli/opts"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -39,7 +40,7 @@ type commitOptions struct {
 	index int
 }
 
-func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func commitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	options := commitOptions{
 		ProjectOptions: p,
 	}
@@ -56,7 +57,7 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runCommit(ctx, dockerCli, backend, options)
+			return runCommit(ctx, dockerCli, backendOptions, options)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -73,12 +74,16 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 	return cmd
 }
 
-func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Compose, options commitOptions) error {
+func runCommit(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options commitOptions) error {
 	projectName, err := options.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Commit(ctx, projectName, api.CommitOptions{
 		Service:   options.service,
 		Reference: options.reference,

+ 7 - 1
cmd/compose/completion.go

@@ -22,6 +22,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -52,8 +53,13 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn
 	}
 }
 
-func completeProjectNames(backend api.Compose) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+func completeProjectNames(dockerCli command.Cli, backendOptions *BackendOptions) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+		if err != nil {
+			return nil, cobra.ShellCompDirectiveError
+		}
+
 		list, err := backend.List(cmd.Context(), api.ListOptions{
 			All: true,
 		})

+ 46 - 42
cmd/compose/compose.go

@@ -42,6 +42,7 @@ import (
 	"github.com/docker/compose/v2/cmd/formatter"
 	"github.com/docker/compose/v2/internal/tracing"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	ui "github.com/docker/compose/v2/pkg/progress"
 	"github.com/docker/compose/v2/pkg/remote"
 	"github.com/docker/compose/v2/pkg/utils"
@@ -415,8 +416,16 @@ func RunningAsStandalone() bool {
 	return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName && os.Args[1] != PluginName
 }
 
+type BackendOptions struct {
+	Options []compose.Option
+}
+
+func (o *BackendOptions) Add(option compose.Option) {
+	o.Options = append(o.Options, option)
+}
+
 // RootCommand returns the compose command with its child commands
-func RootCommand(dockerCli command.Cli, backend api.Compose) *cobra.Command { //nolint:gocyclo
+func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo
 	// filter out useless commandConn.CloseWrite warning message that can occur
 	// when using a remote context that is unreachable: "commandConn.CloseWrite: commandconn: failed to wait: signal: killed"
 	// https://github.com/docker/cli/blob/e1f24d3c93df6752d3c27c8d61d18260f141310c/cli/connhelper/commandconn/commandconn.go#L203-L215
@@ -456,9 +465,7 @@ func RootCommand(dockerCli command.Cli, backend api.Compose) *cobra.Command { //
 			}
 		},
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
-			ctx := cmd.Context()
 			parent := cmd.Root()
-
 			if parent != nil {
 				parentPrerun := parent.PersistentPreRunE
 				if parentPrerun != nil {
@@ -560,64 +567,61 @@ func RootCommand(dockerCli command.Cli, backend api.Compose) *cobra.Command { //
 			}
 			if parallel > 0 {
 				logrus.Debugf("Limiting max concurrency to %d jobs", parallel)
-				backend.MaxConcurrency(parallel)
+				backendOptions.Add(compose.WithMaxConcurrency(parallel))
 			}
 
 			// dry run detection
-			ctx, err = backend.DryRunMode(ctx, dryRun)
-			if err != nil {
-				return err
+			if dryRun {
+				backendOptions.Add(compose.WithDryRun)
 			}
-			cmd.SetContext(ctx)
-
 			return nil
 		},
 	}
 
 	c.AddCommand(
-		upCommand(&opts, dockerCli, backend),
-		downCommand(&opts, dockerCli, backend),
-		startCommand(&opts, dockerCli, backend),
-		restartCommand(&opts, dockerCli, backend),
-		stopCommand(&opts, dockerCli, backend),
-		psCommand(&opts, dockerCli, backend),
-		listCommand(dockerCli, backend),
-		logsCommand(&opts, dockerCli, backend),
+		upCommand(&opts, dockerCli, backendOptions),
+		downCommand(&opts, dockerCli, backendOptions),
+		startCommand(&opts, dockerCli, backendOptions),
+		restartCommand(&opts, dockerCli, backendOptions),
+		stopCommand(&opts, dockerCli, backendOptions),
+		psCommand(&opts, dockerCli, backendOptions),
+		listCommand(dockerCli, backendOptions),
+		logsCommand(&opts, dockerCli, backendOptions),
 		configCommand(&opts, dockerCli),
-		killCommand(&opts, dockerCli, backend),
-		runCommand(&opts, dockerCli, backend),
-		removeCommand(&opts, dockerCli, backend),
-		execCommand(&opts, dockerCli, backend),
-		attachCommand(&opts, dockerCli, backend),
-		exportCommand(&opts, dockerCli, backend),
-		commitCommand(&opts, dockerCli, backend),
-		pauseCommand(&opts, dockerCli, backend),
-		unpauseCommand(&opts, dockerCli, backend),
-		topCommand(&opts, dockerCli, backend),
-		eventsCommand(&opts, dockerCli, backend),
-		portCommand(&opts, dockerCli, backend),
-		imagesCommand(&opts, dockerCli, backend),
+		killCommand(&opts, dockerCli, backendOptions),
+		runCommand(&opts, dockerCli, backendOptions),
+		removeCommand(&opts, dockerCli, backendOptions),
+		execCommand(&opts, dockerCli, backendOptions),
+		attachCommand(&opts, dockerCli, backendOptions),
+		exportCommand(&opts, dockerCli, backendOptions),
+		commitCommand(&opts, dockerCli, backendOptions),
+		pauseCommand(&opts, dockerCli, backendOptions),
+		unpauseCommand(&opts, dockerCli, backendOptions),
+		topCommand(&opts, dockerCli, backendOptions),
+		eventsCommand(&opts, dockerCli, backendOptions),
+		portCommand(&opts, dockerCli, backendOptions),
+		imagesCommand(&opts, dockerCli, backendOptions),
 		versionCommand(dockerCli),
-		buildCommand(&opts, dockerCli, backend),
-		pushCommand(&opts, dockerCli, backend),
-		pullCommand(&opts, dockerCli, backend),
-		createCommand(&opts, dockerCli, backend),
-		copyCommand(&opts, dockerCli, backend),
-		waitCommand(&opts, dockerCli, backend),
-		scaleCommand(&opts, dockerCli, backend),
+		buildCommand(&opts, dockerCli, backendOptions),
+		pushCommand(&opts, dockerCli, backendOptions),
+		pullCommand(&opts, dockerCli, backendOptions),
+		createCommand(&opts, dockerCli, backendOptions),
+		copyCommand(&opts, dockerCli, backendOptions),
+		waitCommand(&opts, dockerCli, backendOptions),
+		scaleCommand(&opts, dockerCli, backendOptions),
 		statsCommand(&opts, dockerCli),
-		watchCommand(&opts, dockerCli, backend),
-		publishCommand(&opts, dockerCli, backend),
-		alphaCommand(&opts, dockerCli, backend),
+		watchCommand(&opts, dockerCli, backendOptions),
+		publishCommand(&opts, dockerCli, backendOptions),
+		alphaCommand(&opts, dockerCli, backendOptions),
 		bridgeCommand(&opts, dockerCli),
-		volumesCommand(&opts, dockerCli, backend),
+		volumesCommand(&opts, dockerCli, backendOptions),
 	)
 
 	c.Flags().SetInterspersed(false)
 	opts.addProjectFlags(c.Flags())
 	c.RegisterFlagCompletionFunc( //nolint:errcheck
 		"project-name",
-		completeProjectNames(backend),
+		completeProjectNames(dockerCli, backendOptions),
 	)
 	c.RegisterFlagCompletionFunc( //nolint:errcheck
 		"project-directory",

+ 8 - 3
cmd/compose/cp.go

@@ -22,6 +22,7 @@ import (
 
 	"github.com/docker/cli/cli"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -38,7 +39,7 @@ type copyOptions struct {
 	copyUIDGID  bool
 }
 
-func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func copyCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := copyOptions{
 		ProjectOptions: p,
 	}
@@ -59,7 +60,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		RunE: AdaptCmd(func(ctx context.Context, cmd *cobra.Command, args []string) error {
 			opts.source = args[0]
 			opts.destination = args[1]
-			return runCopy(ctx, dockerCli, backend, opts)
+			return runCopy(ctx, dockerCli, backendOptions, opts)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -73,12 +74,16 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return copyCmd
 }
 
-func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts copyOptions) error {
+func runCopy(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts copyOptions) error {
 	name, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Copy(ctx, name, api.CopyOptions{
 		Source:      opts.source,
 		Destination: opts.destination,

+ 8 - 3
cmd/compose/create.go

@@ -26,6 +26,7 @@ import (
 
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
@@ -51,7 +52,7 @@ type createOptions struct {
 	AssumeYes     bool
 }
 
-func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func createCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := createOptions{}
 	buildOpts := buildOptions{
 		ProjectOptions: p,
@@ -70,7 +71,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 			return nil
 		}),
 		RunE: p.WithServices(dockerCli, func(ctx context.Context, project *types.Project, services []string) error {
-			return runCreate(ctx, dockerCli, backend, opts, buildOpts, project, services)
+			return runCreate(ctx, dockerCli, backendOptions, opts, buildOpts, project, services)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -95,7 +96,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 	return cmd
 }
 
-func runCreate(ctx context.Context, _ command.Cli, backend api.Compose, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
+func runCreate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error {
 	if err := createOpts.Apply(project); err != nil {
 		return err
 	}
@@ -109,6 +110,10 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Compose, createOp
 		build = &bo
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Create(ctx, project, api.CreateOptions{
 		Build:                build,
 		Services:             services,

+ 0 - 174
cmd/compose/create_test.go

@@ -1,174 +0,0 @@
-/*
-   Copyright 2023 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"
-	"fmt"
-	"testing"
-
-	"github.com/compose-spec/compose-go/v2/types"
-	"github.com/davecgh/go-spew/spew"
-	"github.com/docker/compose/v2/pkg/api"
-	"github.com/docker/compose/v2/pkg/mocks"
-	"github.com/google/go-cmp/cmp"
-	"github.com/stretchr/testify/require"
-	"go.uber.org/mock/gomock"
-)
-
-func TestRunCreate(t *testing.T) {
-	ctrl, ctx := gomock.WithContext(context.Background(), t)
-	backend := mocks.NewMockService(ctrl)
-	backend.EXPECT().Create(
-		gomock.Eq(ctx),
-		pullPolicy(""),
-		deepEqual(defaultCreateOptions(true)),
-	)
-
-	createOpts := createOptions{}
-	buildOpts := buildOptions{
-		ProjectOptions: &ProjectOptions{},
-	}
-	project := sampleProject()
-	err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
-	require.NoError(t, err)
-}
-
-func TestRunCreate_Build(t *testing.T) {
-	ctrl, ctx := gomock.WithContext(context.Background(), t)
-	backend := mocks.NewMockService(ctrl)
-	backend.EXPECT().Create(
-		gomock.Eq(ctx),
-		pullPolicy("build"),
-		deepEqual(defaultCreateOptions(true)),
-	)
-
-	createOpts := createOptions{
-		Build: true,
-	}
-	buildOpts := buildOptions{
-		ProjectOptions: &ProjectOptions{},
-	}
-	project := sampleProject()
-	err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
-	require.NoError(t, err)
-}
-
-func TestRunCreate_NoBuild(t *testing.T) {
-	ctrl, ctx := gomock.WithContext(context.Background(), t)
-	backend := mocks.NewMockService(ctrl)
-	backend.EXPECT().Create(
-		gomock.Eq(ctx),
-		pullPolicy(""),
-		deepEqual(defaultCreateOptions(false)),
-	)
-
-	createOpts := createOptions{
-		noBuild: true,
-	}
-	buildOpts := buildOptions{}
-	project := sampleProject()
-	err := runCreate(ctx, nil, backend, createOpts, buildOpts, project, nil)
-	require.NoError(t, err)
-}
-
-func sampleProject() *types.Project {
-	return &types.Project{
-		Name: "test",
-		Services: types.Services{
-			"svc": {
-				Name: "svc",
-				Build: &types.BuildConfig{
-					Context: ".",
-				},
-			},
-		},
-	}
-}
-
-func defaultCreateOptions(includeBuild bool) api.CreateOptions {
-	var build *api.BuildOptions
-	if includeBuild {
-		bo := defaultBuildOptions()
-		build = &bo
-	}
-	return api.CreateOptions{
-		Build:                build,
-		Services:             nil,
-		RemoveOrphans:        false,
-		IgnoreOrphans:        false,
-		Recreate:             "diverged",
-		RecreateDependencies: "diverged",
-		Inherit:              true,
-		Timeout:              nil,
-		QuietPull:            false,
-	}
-}
-
-func defaultBuildOptions() api.BuildOptions {
-	return api.BuildOptions{
-		Args:     make(types.MappingWithEquals),
-		Progress: "auto",
-	}
-}
-
-// deepEqual returns a nice diff on failure vs gomock.Eq when used
-// on structs.
-func deepEqual(x interface{}) gomock.Matcher {
-	return gomock.GotFormatterAdapter(
-		gomock.GotFormatterFunc(func(got interface{}) string {
-			return cmp.Diff(x, got)
-		}),
-		gomock.Eq(x),
-	)
-}
-
-func spewAdapter(m gomock.Matcher) gomock.Matcher {
-	return gomock.GotFormatterAdapter(
-		gomock.GotFormatterFunc(func(got interface{}) string {
-			return spew.Sdump(got)
-		}),
-		m,
-	)
-}
-
-type withPullPolicy struct {
-	policy string
-}
-
-func pullPolicy(policy string) gomock.Matcher {
-	return spewAdapter(withPullPolicy{policy: policy})
-}
-
-func (w withPullPolicy) Matches(x interface{}) bool {
-	proj, ok := x.(*types.Project)
-	if !ok || proj == nil || len(proj.Services) == 0 {
-		return false
-	}
-
-	for _, svc := range proj.Services {
-		if svc.PullPolicy != w.policy {
-			return false
-		}
-	}
-
-	return true
-}
-
-func (w withPullPolicy) String() string {
-	return fmt.Sprintf("has pull policy %q for all services", w.policy)
-}

+ 8 - 3
cmd/compose/down.go

@@ -23,6 +23,7 @@ import (
 	"time"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/docker/compose/v2/pkg/utils"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -40,7 +41,7 @@ type downOptions struct {
 	images        string
 }
 
-func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := downOptions{
 		ProjectOptions: p,
 	}
@@ -57,7 +58,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runDown(ctx, dockerCli, backend, opts, args)
+			return runDown(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: noCompletion(),
 	}
@@ -77,7 +78,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return downCmd
 }
 
-func runDown(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts downOptions, services []string) error {
+func runDown(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts downOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
@@ -88,6 +89,10 @@ func runDown(ctx context.Context, dockerCli command.Cli, backend api.Compose, op
 		timeoutValue := time.Duration(opts.timeout) * time.Second
 		timeout = &timeoutValue
 	}
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Down(ctx, name, api.DownOptions{
 		RemoveOrphans: opts.removeOrphans,
 		Project:       project,

+ 8 - 3
cmd/compose/events.go

@@ -23,6 +23,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 
 	"github.com/spf13/cobra"
 )
@@ -34,7 +35,7 @@ type eventsOpts struct {
 	until string
 }
 
-func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := eventsOpts{
 		composeOptions: &composeOptions{
 			ProjectOptions: p,
@@ -44,7 +45,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 		Use:   "events [OPTIONS] [SERVICE...]",
 		Short: "Receive real time events from containers",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runEvents(ctx, dockerCli, backend, opts, args)
+			return runEvents(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -55,12 +56,16 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 	return cmd
 }
 
-func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts eventsOpts, services []string) error {
+func runEvents(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts eventsOpts, services []string) error {
 	name, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Events(ctx, name, api.EventsOptions{
 		Services: services,
 		Since:    opts.since,

+ 7 - 3
cmd/compose/exec.go

@@ -48,7 +48,7 @@ type execOpts struct {
 	interactive bool
 }
 
-func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func execCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := execOpts{
 		composeOptions: &composeOptions{
 			ProjectOptions: p,
@@ -64,7 +64,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			err := runExec(ctx, dockerCli, backend, opts)
+			err := runExec(ctx, dockerCli, backendOptions, opts)
 			if err != nil {
 				logrus.Debugf("%v", err)
 				var cliError cli.StatusError
@@ -100,7 +100,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return runCmd
 }
 
-func runExec(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts execOpts) error {
+func runExec(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts execOpts) error {
 	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
@@ -126,6 +126,10 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Compose, op
 		Interactive: opts.interactive,
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	exitCode, err := backend.Exec(ctx, projectName, execOpts)
 	if exitCode != 0 {
 		errMsg := fmt.Sprintf("exit status %d", exitCode)

+ 8 - 3
cmd/compose/export.go

@@ -20,6 +20,7 @@ import (
 	"context"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -33,7 +34,7 @@ type exportOptions struct {
 	index   int
 }
 
-func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func exportCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	options := exportOptions{
 		ProjectOptions: p,
 	}
@@ -46,7 +47,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runExport(ctx, dockerCli, backend, options)
+			return runExport(ctx, dockerCli, backendOptions, options)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -58,7 +59,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 	return cmd
 }
 
-func runExport(ctx context.Context, dockerCli command.Cli, backend api.Compose, options exportOptions) error {
+func runExport(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, options exportOptions) error {
 	projectName, err := options.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
@@ -70,5 +71,9 @@ func runExport(ctx context.Context, dockerCli command.Cli, backend api.Compose,
 		Output:  options.output,
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Export(ctx, projectName, exportOptions)
 }

+ 11 - 3
cmd/compose/generate.go

@@ -21,7 +21,9 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -30,7 +32,7 @@ type generateOptions struct {
 	Format string
 }
 
-func generateCommand(p *ProjectOptions, backend api.Compose) *cobra.Command {
+func generateCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := generateOptions{
 		ProjectOptions: p,
 	}
@@ -42,7 +44,7 @@ func generateCommand(p *ProjectOptions, backend api.Compose) *cobra.Command {
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runGenerate(ctx, backend, opts, args)
+			return runGenerate(ctx, dockerCli, backendOptions, opts, args)
 		}),
 	}
 
@@ -52,11 +54,16 @@ func generateCommand(p *ProjectOptions, backend api.Compose) *cobra.Command {
 	return cmd
 }
 
-func runGenerate(ctx context.Context, backend api.Compose, opts generateOptions, containers []string) error {
+func runGenerate(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts generateOptions, containers []string) error {
 	_, _ = fmt.Fprintln(os.Stderr, "generate command is EXPERIMENTAL")
 	if len(containers) == 0 {
 		return fmt.Errorf("at least one container must be specified")
 	}
+
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	project, err := backend.Generate(ctx, api.GenerateOptions{
 		Containers:  containers,
 		ProjectName: opts.ProjectName,
@@ -64,6 +71,7 @@ func runGenerate(ctx context.Context, backend api.Compose, opts generateOptions,
 	if err != nil {
 		return err
 	}
+
 	var content []byte
 	switch opts.Format {
 	case "json":

+ 8 - 3
cmd/compose/images.go

@@ -27,6 +27,7 @@ import (
 
 	"github.com/containerd/platforms"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/docker/docker/pkg/stringid"
 	"github.com/docker/go-units"
 	"github.com/spf13/cobra"
@@ -41,7 +42,7 @@ type imageOptions struct {
 	Format string
 }
 
-func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := imageOptions{
 		ProjectOptions: p,
 	}
@@ -49,7 +50,7 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 		Use:   "images [OPTIONS] [SERVICE...]",
 		Short: "List images used by the created containers",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runImages(ctx, dockerCli, backend, opts, args)
+			return runImages(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -58,12 +59,16 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose
 	return imgCmd
 }
 
-func runImages(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts imageOptions, services []string) error {
+func runImages(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts imageOptions, services []string) error {
 	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	images, err := backend.Images(ctx, projectName, api.ImagesOptions{
 		Services: services,
 	})

+ 8 - 3
cmd/compose/kill.go

@@ -21,6 +21,7 @@ import (
 	"os"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -33,7 +34,7 @@ type killOptions struct {
 	signal        string
 }
 
-func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func killCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := killOptions{
 		ProjectOptions: p,
 	}
@@ -41,7 +42,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		Use:   "kill [OPTIONS] [SERVICE...]",
 		Short: "Force stop service containers",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runKill(ctx, dockerCli, backend, opts, args)
+			return runKill(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -54,12 +55,16 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return cmd
 }
 
-func runKill(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts killOptions, services []string) error {
+func runKill(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts killOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Kill(ctx, name, api.KillOptions{
 		RemoveOrphans: opts.removeOrphans,
 		Project:       project,

+ 8 - 3
cmd/compose/list.go

@@ -24,6 +24,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/cmd/formatter"
+	"github.com/docker/compose/v2/pkg/compose"
 
 	"github.com/docker/cli/opts"
 	"github.com/spf13/cobra"
@@ -38,13 +39,13 @@ type lsOptions struct {
 	Filter opts.FilterOpt
 }
 
-func listCommand(dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func listCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	lsOpts := lsOptions{Filter: opts.NewFilterOpt()}
 	lsCmd := &cobra.Command{
 		Use:   "ls [OPTIONS]",
 		Short: "List running compose projects",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runList(ctx, dockerCli, backend, lsOpts)
+			return runList(ctx, dockerCli, backendOptions, lsOpts)
 		}),
 		Args:              cobra.NoArgs,
 		ValidArgsFunction: noCompletion(),
@@ -61,13 +62,17 @@ var acceptedListFilters = map[string]bool{
 	"name": true,
 }
 
-func runList(ctx context.Context, dockerCli command.Cli, backend api.Compose, lsOpts lsOptions) error {
+func runList(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, lsOpts lsOptions) error {
 	filters := lsOpts.Filter.Value()
 	err := filters.Validate(acceptedListFilters)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All})
 	if err != nil {
 		return err

+ 8 - 3
cmd/compose/logs.go

@@ -21,6 +21,7 @@ import (
 	"errors"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/cmd/formatter"
@@ -40,7 +41,7 @@ type logsOptions struct {
 	timestamps bool
 }
 
-func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func logsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := logsOptions{
 		ProjectOptions: p,
 	}
@@ -48,7 +49,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		Use:   "logs [OPTIONS] [SERVICE...]",
 		Short: "View output from containers",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runLogs(ctx, dockerCli, backend, opts, args)
+			return runLogs(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		PreRunE: func(cmd *cobra.Command, args []string) error {
 			if opts.index > 0 && len(args) != 1 {
@@ -70,7 +71,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return logsCmd
 }
 
-func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts logsOptions, services []string) error {
+func runLogs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts logsOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
@@ -86,6 +87,10 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Compose, op
 	}
 
 	consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false)
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Logs(ctx, name, consumer, api.LogOptions{
 		Project:    project,
 		Services:   services,

+ 15 - 6
cmd/compose/pause.go

@@ -20,6 +20,7 @@ import (
 	"context"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -29,7 +30,7 @@ type pauseOptions struct {
 	*ProjectOptions
 }
 
-func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := pauseOptions{
 		ProjectOptions: p,
 	}
@@ -37,19 +38,23 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		Use:   "pause [SERVICE...]",
 		Short: "Pause services",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runPause(ctx, dockerCli, backend, opts, args)
+			return runPause(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
 	return cmd
 }
 
-func runPause(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts pauseOptions, services []string) error {
+func runPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pauseOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Pause(ctx, name, api.PauseOptions{
 		Services: services,
 		Project:  project,
@@ -60,7 +65,7 @@ type unpauseOptions struct {
 	*ProjectOptions
 }
 
-func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := unpauseOptions{
 		ProjectOptions: p,
 	}
@@ -68,19 +73,23 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 		Use:   "unpause [SERVICE...]",
 		Short: "Unpause services",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runUnPause(ctx, dockerCli, backend, opts, args)
+			return runUnPause(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
 	return cmd
 }
 
-func runUnPause(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts unpauseOptions, services []string) error {
+func runUnPause(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts unpauseOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.UnPause(ctx, name, api.PauseOptions{
 		Services: services,
 		Project:  project,

+ 9 - 3
cmd/compose/port.go

@@ -23,6 +23,7 @@ import (
 	"strings"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -35,7 +36,7 @@ type portOptions struct {
 	index    int
 }
 
-func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func portCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := portOptions{
 		ProjectOptions: p,
 	}
@@ -53,7 +54,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runPort(ctx, dockerCli, backend, opts, args[0])
+			return runPort(ctx, dockerCli, backendOptions, opts, args[0])
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -62,11 +63,16 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return cmd
 }
 
-func runPort(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts portOptions, service string) error {
+func runPort(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts portOptions, service string) error {
 	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
+
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{
 		Protocol: opts.protocol,
 		Index:    opts.index,

+ 8 - 3
cmd/compose/ps.go

@@ -26,6 +26,7 @@ import (
 
 	"github.com/docker/compose/v2/cmd/formatter"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 
 	"github.com/docker/cli/cli/command"
 	cliformatter "github.com/docker/cli/cli/command/formatter"
@@ -64,7 +65,7 @@ func (p *psOptions) parseFilter() error {
 	return nil
 }
 
-func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := psOptions{
 		ProjectOptions: p,
 	}
@@ -75,7 +76,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *c
 			return opts.parseFilter()
 		},
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runPs(ctx, dockerCli, backend, args, opts)
+			return runPs(ctx, dockerCli, backendOptions, args, opts)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -91,7 +92,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *c
 	return psCmd
 }
 
-func runPs(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, opts psOptions) error {
+func runPs(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, opts psOptions) error { //nolint:gocyclo
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
@@ -111,6 +112,10 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Compose, serv
 		}
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	containers, err := backend.Ps(ctx, name, api.PsOptions{
 		Project:  project,
 		All:      opts.All || len(opts.Status) != 0,

+ 0 - 87
cmd/compose/ps_test.go

@@ -1,87 +0,0 @@
-/*
-   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"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/docker/cli/cli/config/configfile"
-	"github.com/docker/cli/cli/streams"
-	"github.com/docker/compose/v2/pkg/api"
-	"github.com/docker/compose/v2/pkg/mocks"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"go.uber.org/mock/gomock"
-)
-
-func TestPsTable(t *testing.T) {
-	ctx := context.Background()
-	dir := t.TempDir()
-	out := filepath.Join(dir, "output.txt")
-	f, err := os.Create(out)
-	if err != nil {
-		t.Fatal("could not create output file")
-	}
-	defer func() { _ = f.Close() }()
-
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-
-	backend := mocks.NewMockService(ctrl)
-	backend.EXPECT().
-		Ps(gomock.Eq(ctx), gomock.Any(), gomock.Any()).
-		DoAndReturn(func(ctx context.Context, projectName string, options api.PsOptions) ([]api.ContainerSummary, error) {
-			return []api.ContainerSummary{
-				{
-					ID:    "abc123",
-					Name:  "ABC",
-					Image: "foo/bar",
-					Publishers: api.PortPublishers{
-						{
-							TargetPort:    8080,
-							PublishedPort: 8080,
-							Protocol:      "tcp",
-						},
-						{
-							TargetPort:    8443,
-							PublishedPort: 8443,
-							Protocol:      "tcp",
-						},
-					},
-				},
-			}, nil
-		}).AnyTimes()
-
-	opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}}
-	stdout := streams.NewOut(f)
-	cli := mocks.NewMockCli(ctrl)
-	cli.EXPECT().Out().Return(stdout).AnyTimes()
-	cli.EXPECT().ConfigFile().Return(&configfile.ConfigFile{}).AnyTimes()
-	err = runPs(ctx, cli, backend, nil, opts)
-	require.NoError(t, err)
-
-	_, err = f.Seek(0, 0)
-	require.NoError(t, err)
-
-	output, err := os.ReadFile(out)
-	require.NoError(t, err)
-
-	assert.Contains(t, string(output), "8080/tcp, 8443/tcp")
-}

+ 8 - 3
cmd/compose/publish.go

@@ -23,6 +23,7 @@ import (
 	"github.com/docker/cli/cli"
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
@@ -37,7 +38,7 @@ type publishOptions struct {
 	app                 bool
 }
 
-func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := publishOptions{
 		ProjectOptions: p,
 	}
@@ -45,7 +46,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 		Use:   "publish [OPTIONS] REPOSITORY[:TAG]",
 		Short: "Publish compose application",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runPublish(ctx, dockerCli, backend, opts, args[0])
+			return runPublish(ctx, dockerCli, backendOptions, opts, args[0])
 		}),
 		Args: cli.ExactArgs(1),
 	}
@@ -67,7 +68,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 	return cmd
 }
 
-func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts publishOptions, repository string) error {
+func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts publishOptions, repository string) error {
 	project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
 	if err != nil {
 		return err
@@ -77,6 +78,10 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Compose,
 		return errors.New("cannot publish compose file with local includes")
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Publish(ctx, project, repository, api.PublishOptions{
 		ResolveImageDigests: opts.resolveImageDigests || opts.app,
 		Application:         opts.app,

+ 8 - 3
cmd/compose/pull.go

@@ -24,6 +24,7 @@ import (
 	"github.com/compose-spec/compose-go/v2/cli"
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/morikuni/aec"
 	"github.com/spf13/cobra"
 
@@ -42,7 +43,7 @@ type pullOptions struct {
 	policy             string
 }
 
-func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func pullCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := pullOptions{
 		ProjectOptions: p,
 	}
@@ -59,7 +60,7 @@ func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			return nil
 		},
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runPull(ctx, dockerCli, backend, opts, args)
+			return runPull(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -97,7 +98,7 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
 	return project, nil
 }
 
-func runPull(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts pullOptions, services []string) error {
+func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pullOptions, services []string) error {
 	project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
@@ -108,6 +109,10 @@ func runPull(ctx context.Context, dockerCli command.Cli, backend api.Compose, op
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Pull(ctx, project, api.PullOptions{
 		Quiet:           opts.quiet,
 		IgnoreFailures:  opts.ignorePullFailures,

+ 8 - 3
cmd/compose/push.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -34,7 +35,7 @@ type pushOptions struct {
 	Quiet          bool
 }
 
-func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := pushOptions{
 		ProjectOptions: p,
 	}
@@ -42,7 +43,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		Use:   "push [OPTIONS] [SERVICE...]",
 		Short: "Push service images",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runPush(ctx, dockerCli, backend, opts, args)
+			return runPush(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -53,7 +54,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return pushCmd
 }
 
-func runPush(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts pushOptions, services []string) error {
+func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pushOptions, services []string) error {
 	project, _, err := opts.ToProject(ctx, dockerCli, services)
 	if err != nil {
 		return err
@@ -66,6 +67,10 @@ func runPush(ctx context.Context, dockerCli command.Cli, backend api.Compose, op
 		}
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Push(ctx, project, api.PushOptions{
 		IgnoreFailures: opts.Ignorefailures,
 		Quiet:          opts.Quiet,

+ 8 - 3
cmd/compose/remove.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -31,7 +32,7 @@ type removeOptions struct {
 	volumes bool
 }
 
-func removeCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func removeCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := removeOptions{
 		ProjectOptions: p,
 	}
@@ -45,7 +46,7 @@ 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: Adapt(func(ctx context.Context, args []string) error {
-			return runRemove(ctx, dockerCli, backend, opts, args)
+			return runRemove(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -59,12 +60,16 @@ Any data which is not in a volume will be lost.`,
 	return cmd
 }
 
-func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts removeOptions, services []string) error {
+func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts removeOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Remove(ctx, name, api.RemoveOptions{
 		Services: services,
 		Force:    opts.force,

+ 8 - 3
cmd/compose/restart.go

@@ -21,6 +21,7 @@ import (
 	"time"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -33,7 +34,7 @@ type restartOptions struct {
 	noDeps      bool
 }
 
-func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func restartCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := restartOptions{
 		ProjectOptions: p,
 	}
@@ -44,7 +45,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 			opts.timeChanged = cmd.Flags().Changed("timeout")
 		},
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runRestart(ctx, dockerCli, backend, opts, args)
+			return runRestart(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -55,7 +56,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 	return restartCmd
 }
 
-func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts restartOptions, services []string) error {
+func runRestart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts restartOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli)
 	if err != nil {
 		return err
@@ -74,6 +75,10 @@ func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Compose,
 		timeout = &timeoutValue
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Restart(ctx, name, api.RestartOptions{
 		Timeout:  timeout,
 		Services: services,

+ 8 - 3
cmd/compose/run.go

@@ -24,6 +24,7 @@ import (
 
 	"github.com/compose-spec/compose-go/v2/dotenv"
 	"github.com/compose-spec/compose-go/v2/format"
+	"github.com/docker/compose/v2/pkg/compose"
 	xprogress "github.com/moby/buildkit/util/progress/progressui"
 	"github.com/sirupsen/logrus"
 
@@ -142,7 +143,7 @@ func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (t
 	return environment, nil
 }
 
-func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	options := runOptions{
 		composeOptions: &composeOptions{
 			ProjectOptions: p,
@@ -218,7 +219,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *
 			}
 
 			options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
-			return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli)
+			return runRun(ctx, backendOptions, project, options, createOpts, buildOpts, dockerCli)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -266,7 +267,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
 	return pflag.NormalizedName(name)
 }
 
-func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
+func runRun(ctx context.Context, backendOptions *BackendOptions, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
 	project, err := options.apply(project)
 	if err != nil {
 		return err
@@ -338,6 +339,10 @@ func runRun(ctx context.Context, backend api.Compose, project *types.Project, op
 		}
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
 	if exitCode != 0 {
 		errMsg := ""

+ 8 - 3
cmd/compose/scale.go

@@ -27,6 +27,7 @@ import (
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -35,7 +36,7 @@ type scaleOptions struct {
 	noDeps bool
 }
 
-func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := scaleOptions{
 		ProjectOptions: p,
 	}
@@ -48,7 +49,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			if err != nil {
 				return err
 			}
-			return runScale(ctx, dockerCli, backend, opts, serviceTuples)
+			return runScale(ctx, dockerCli, backendOptions, opts, serviceTuples)
 		}),
 		ValidArgsFunction: completeScaleArgs(dockerCli, p),
 	}
@@ -58,7 +59,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return scaleCmd
 }
 
-func runScale(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts scaleOptions, serviceReplicaTuples map[string]int) error {
+func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts scaleOptions, serviceReplicaTuples map[string]int) error {
 	services := slices.Sorted(maps.Keys(serviceReplicaTuples))
 	project, _, err := opts.ToProject(ctx, dockerCli, services)
 	if err != nil {
@@ -80,6 +81,10 @@ func runScale(ctx context.Context, dockerCli command.Cli, backend api.Compose, o
 		project.Services[key] = service
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Scale(ctx, project, api.ScaleOptions{Services: services})
 }
 

+ 8 - 3
cmd/compose/start.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -28,7 +29,7 @@ type startOptions struct {
 	*ProjectOptions
 }
 
-func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func startCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := startOptions{
 		ProjectOptions: p,
 	}
@@ -36,19 +37,23 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		Use:   "start [SERVICE...]",
 		Short: "Start services",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runStart(ctx, dockerCli, backend, opts, args)
+			return runStart(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
 	return startCmd
 }
 
-func runStart(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts startOptions, services []string) error {
+func runStart(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts startOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Start(ctx, name, api.StartOptions{
 		AttachTo: services,
 		Project:  project,

+ 8 - 3
cmd/compose/stop.go

@@ -21,6 +21,7 @@ import (
 	"time"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -32,7 +33,7 @@ type stopOptions struct {
 	timeout     int
 }
 
-func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func stopCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := stopOptions{
 		ProjectOptions: p,
 	}
@@ -43,7 +44,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			opts.timeChanged = cmd.Flags().Changed("timeout")
 		},
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runStop(ctx, dockerCli, backend, opts, args)
+			return runStop(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -53,7 +54,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return cmd
 }
 
-func runStop(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts stopOptions, services []string) error {
+func runStop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts stopOptions, services []string) error {
 	project, name, err := opts.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
@@ -64,6 +65,10 @@ func runStop(ctx context.Context, dockerCli command.Cli, backend api.Compose, op
 		timeoutValue := time.Duration(opts.timeout) * time.Second
 		timeout = &timeoutValue
 	}
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Stop(ctx, name, api.StopOptions{
 		Timeout:  timeout,
 		Services: services,

+ 9 - 3
cmd/compose/top.go

@@ -25,6 +25,7 @@ import (
 	"text/tabwriter"
 
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose/v2/pkg/api"
@@ -34,7 +35,7 @@ type topOptions struct {
 	*ProjectOptions
 }
 
-func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func topCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := topOptions{
 		ProjectOptions: p,
 	}
@@ -42,7 +43,7 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *
 		Use:   "top [SERVICES...]",
 		Short: "Display the running processes",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runTop(ctx, dockerCli, backend, opts, args)
+			return runTop(ctx, dockerCli, backendOptions, opts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -54,11 +55,16 @@ type (
 	topEntries map[string]string
 )
 
-func runTop(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts topOptions, services []string) error {
+func runTop(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts topOptions, services []string) error {
 	projectName, err := opts.toProjectName(ctx, dockerCli)
 	if err != nil {
 		return err
 	}
+
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	containers, err := backend.Top(ctx, projectName, services)
 	if err != nil {
 		return err

+ 9 - 3
cmd/compose/up.go

@@ -26,6 +26,7 @@ import (
 
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/cli/cli/command"
+	"github.com/docker/compose/v2/pkg/compose"
 	xprogress "github.com/moby/buildkit/util/progress/progressui"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -109,7 +110,7 @@ func (opts upOptions) OnExit() api.Cascade {
 	}
 }
 
-func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	up := upOptions{}
 	create := createOptions{}
 	build := buildOptions{ProjectOptions: p}
@@ -140,7 +141,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *c
 				return fmt.Errorf("no service selected")
 			}
 
-			return runUp(ctx, dockerCli, backend, create, up, build, project, services)
+			return runUp(ctx, dockerCli, backendOptions, create, up, build, project, services)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -228,7 +229,7 @@ func validateFlags(up *upOptions, create *createOptions) error {
 func runUp(
 	ctx context.Context,
 	dockerCli command.Cli,
-	backend api.Compose,
+	backendOptions *BackendOptions,
 	createOptions createOptions,
 	upOptions upOptions,
 	buildOptions buildOptions,
@@ -280,6 +281,11 @@ func runUp(
 		AssumeYes:            createOptions.AssumeYes,
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
+
 	if upOptions.noStart {
 		return backend.Create(ctx, project, create)
 	}

+ 8 - 3
cmd/compose/viz.go

@@ -24,6 +24,7 @@ import (
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -35,7 +36,7 @@ type vizOptions struct {
 	indentationStr   string
 }
 
-func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := vizOptions{
 		ProjectOptions: p,
 	}
@@ -51,7 +52,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *
 			return err
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runViz(ctx, dockerCli, backend, &opts)
+			return runViz(ctx, dockerCli, backendOptions, &opts)
 		}),
 	}
 
@@ -63,7 +64,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *
 	return cmd
 }
 
-func runViz(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts *vizOptions) error {
+func runViz(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *vizOptions) error {
 	_, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL")
 	project, _, err := opts.ToProject(ctx, dockerCli, nil)
 	if err != nil {
@@ -71,6 +72,10 @@ func runViz(ctx context.Context, dockerCli command.Cli, backend api.Compose, opt
 	}
 
 	// build graph
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	graphStr, _ := backend.Viz(ctx, project, api.VizOptions{
 		IncludeNetworks:  opts.includeNetworks,
 		IncludePorts:     opts.includePorts,

+ 8 - 3
cmd/compose/volumes.go

@@ -25,6 +25,7 @@ import (
 	"github.com/docker/cli/cli/command/formatter"
 	"github.com/docker/cli/cli/flags"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -34,7 +35,7 @@ type volumesOptions struct {
 	Format string
 }
 
-func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	options := volumesOptions{
 		ProjectOptions: p,
 	}
@@ -43,7 +44,7 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 		Use:   "volumes [OPTIONS] [SERVICE...]",
 		Short: "List volumes",
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			return runVol(ctx, dockerCli, backend, args, options)
+			return runVol(ctx, dockerCli, backendOptions, args, options)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -54,7 +55,7 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compos
 	return cmd
 }
 
-func runVol(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, options volumesOptions) error {
+func runVol(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, services []string, options volumesOptions) error {
 	project, name, err := options.projectOrName(ctx, dockerCli, services...)
 	if err != nil {
 		return err
@@ -69,6 +70,10 @@ func runVol(ctx context.Context, dockerCli command.Cli, backend api.Compose, ser
 		}
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	volumes, err := backend.Volumes(ctx, name, api.VolumesOptions{
 		Services: services,
 	})

+ 8 - 3
cmd/compose/wait.go

@@ -23,6 +23,7 @@ import (
 	"github.com/docker/cli/cli"
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/pkg/api"
+	"github.com/docker/compose/v2/pkg/compose"
 	"github.com/spf13/cobra"
 )
 
@@ -34,7 +35,7 @@ type waitOptions struct {
 	downProject bool
 }
 
-func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	opts := waitOptions{
 		ProjectOptions: p,
 	}
@@ -47,7 +48,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 		Args:  cli.RequiresMinArgs(1),
 		RunE: Adapt(func(ctx context.Context, services []string) error {
 			opts.services = services
-			statusCode, err = runWait(ctx, dockerCli, backend, &opts)
+			statusCode, err = runWait(ctx, dockerCli, backendOptions, &opts)
 			return err
 		}),
 		PostRun: func(cmd *cobra.Command, args []string) {
@@ -60,12 +61,16 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return cmd
 }
 
-func runWait(ctx context.Context, dockerCli command.Cli, backend api.Compose, opts *waitOptions) (int64, error) {
+func runWait(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts *waitOptions) (int64, error) {
 	_, name, err := opts.projectOrName(ctx, dockerCli)
 	if err != nil {
 		return 0, err
 	}
 
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return 0, err
+	}
 	return backend.Wait(ctx, name, api.WaitOptions{
 		Services:                   opts.services,
 		DownProjectOnContainerExit: opts.downProject,

+ 12 - 3
cmd/compose/watch.go

@@ -22,6 +22,7 @@ import (
 
 	"github.com/compose-spec/compose-go/v2/types"
 	"github.com/docker/compose/v2/cmd/formatter"
+	"github.com/docker/compose/v2/pkg/compose"
 
 	"github.com/docker/cli/cli/command"
 	"github.com/docker/compose/v2/internal/locker"
@@ -36,7 +37,7 @@ type watchOptions struct {
 	noUp  bool
 }
 
-func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose) *cobra.Command {
+func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
 	watchOpts := watchOptions{
 		ProjectOptions: p,
 	}
@@ -53,7 +54,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 			if cmd.Parent().Name() == "alpha" {
 				logrus.Warn("watch command is now available as a top level command")
 			}
-			return runWatch(ctx, dockerCli, backend, watchOpts, buildOpts, args)
+			return runWatch(ctx, dockerCli, backendOptions, watchOpts, buildOpts, args)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -64,7 +65,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Compose)
 	return cmd
 }
 
-func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Compose, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
+func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
 	project, _, err := watchOpts.ToProject(ctx, dockerCli, services)
 	if err != nil {
 		return err
@@ -111,12 +112,20 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Compose, w
 				Services: services,
 			},
 		}
+		backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+		if err != nil {
+			return err
+		}
 		if err := backend.Up(ctx, project, upOpts); err != nil {
 			return err
 		}
 	}
 
 	consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
 	return backend.Watch(ctx, project, api.WatchOptions{
 		Build:    &build,
 		LogTo:    consumer,

+ 7 - 4
cmd/main.go

@@ -37,10 +37,13 @@ import (
 func pluginMain() {
 	plugin.Run(
 		func(cli command.Cli) *cobra.Command {
-			backend := compose.NewComposeService(cli,
-				compose.WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
-			)
-			cmd := commands.RootCommand(cli, backend)
+			backendOptions := &commands.BackendOptions{
+				Options: []compose.Option{
+					compose.WithPrompt(prompt.NewPrompt(cli.In(), cli.Out()).Confirm),
+				},
+			}
+
+			cmd := commands.RootCommand(cli, backendOptions)
 			originalPreRunE := cmd.PersistentPreRunE
 			cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
 				// initialize the cli instance

+ 1 - 1
go.mod

@@ -12,7 +12,6 @@ require (
 	github.com/containerd/containerd/v2 v2.1.4
 	github.com/containerd/errdefs v1.0.0
 	github.com/containerd/platforms v1.0.0-rc.1
-	github.com/davecgh/go-spew v1.1.1
 	github.com/distribution/reference v0.6.0
 	github.com/docker/buildx v0.29.1
 	github.com/docker/cli v28.5.1+incompatible
@@ -89,6 +88,7 @@ require (
 	github.com/containerd/ttrpc v1.2.7 // indirect
 	github.com/containerd/typeurl/v2 v2.2.3 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/docker/distribution v2.8.3+incompatible // indirect
 	github.com/docker/docker-credential-helpers v0.9.3 // indirect
 	github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect

+ 0 - 4
pkg/api/api.go

@@ -83,10 +83,6 @@ type Compose interface {
 	Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error
 	// Images executes the equivalent of a `compose images`
 	Images(ctx context.Context, projectName string, options ImagesOptions) (map[string]ImageSummary, error)
-	// MaxConcurrency defines upper limit for concurrent operations against engine API
-	MaxConcurrency(parallel int)
-	// DryRunMode defines if dry run applies to the command
-	DryRunMode(ctx context.Context, dryRun bool) (context.Context, error)
 	// Watch services' development context and sync/notify/rebuild/restart on changes
 	Watch(ctx context.Context, project *types.Project, options WatchOptions) error
 	// Viz generates a graphviz graph of the project services

+ 0 - 2
pkg/api/dryrunclient.go

@@ -56,8 +56,6 @@ const (
 
 var _ client.APIClient = &DryRunClient{}
 
-type DryRunKey struct{}
-
 // DryRunClient implements APIClient by delegating to implementation functions. This allows lazy init and per-method overrides
 type DryRunClient struct {
 	apiClient  client.APIClient

+ 36 - 30
pkg/compose/compose.go

@@ -51,10 +51,10 @@ func init() {
 	}
 }
 
-type Option func(service *composeService)
+type Option func(service *composeService) error
 
 // NewComposeService create a local implementation of the compose.Compose API
-func NewComposeService(dockerCli command.Cli, options ...Option) api.Compose {
+func NewComposeService(dockerCli command.Cli, options ...Option) (api.Compose, error) {
 	s := &composeService{
 		dockerCli:      dockerCli,
 		clock:          clockwork.NewRealClock(),
@@ -62,7 +62,9 @@ func NewComposeService(dockerCli command.Cli, options ...Option) api.Compose {
 		dryRun:         false,
 	}
 	for _, option := range options {
-		option(s)
+		if err := option(s); err != nil {
+			return nil, err
+		}
 	}
 	if s.prompt == nil {
 		s.prompt = func(message string, defaultValue bool) (bool, error) {
@@ -71,14 +73,43 @@ func NewComposeService(dockerCli command.Cli, options ...Option) api.Compose {
 			return defaultValue, nil
 		}
 	}
-	return s
+	return s, nil
 }
 
 // WithPrompt configure a UI component for Compose service to interact with user and confirm actions
 func WithPrompt(prompt Prompt) Option {
-	return func(s *composeService) {
+	return func(s *composeService) error {
 		s.prompt = prompt
+		return nil
+	}
+}
+
+// WithMaxConcurrency defines upper limit for concurrent operations against engine API
+func WithMaxConcurrency(maxConcurrency int) Option {
+	return func(s *composeService) error {
+		s.maxConcurrency = maxConcurrency
+		return nil
+	}
+}
+
+// WithDryRun configure Compose to run without actually applying changes
+func WithDryRun(s *composeService) error {
+	s.dryRun = true
+	cli, err := command.NewDockerCli()
+	if err != nil {
+		return err
+	}
+
+	options := flags.NewClientOptions()
+	options.Context = s.dockerCli.CurrentContext()
+	err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
+		return api.NewDryRunClient(s.apiClient(), s.dockerCli)
+	}))
+	if err != nil {
+		return err
 	}
+	s.dockerCli = cli
+	return nil
 }
 
 type Prompt func(message string, defaultValue bool) (bool, error)
@@ -113,31 +144,6 @@ func (s *composeService) configFile() *configfile.ConfigFile {
 	return s.dockerCli.ConfigFile()
 }
 
-func (s *composeService) MaxConcurrency(i int) {
-	s.maxConcurrency = i
-}
-
-func (s *composeService) DryRunMode(ctx context.Context, dryRun bool) (context.Context, error) {
-	s.dryRun = dryRun
-	if dryRun {
-		cli, err := command.NewDockerCli()
-		if err != nil {
-			return ctx, err
-		}
-
-		options := flags.NewClientOptions()
-		options.Context = s.dockerCli.CurrentContext()
-		err = cli.Initialize(options, command.WithInitializeClient(func(cli *command.DockerCli) (client.APIClient, error) {
-			return api.NewDryRunClient(s.apiClient(), s.dockerCli)
-		}))
-		if err != nil {
-			return ctx, err
-		}
-		s.dockerCli = cli
-	}
-	return context.WithValue(ctx, api.DryRunKey{}, dryRun), nil
-}
-
 func (s *composeService) stdout() *streams.Out {
 	return s.dockerCli.Out()
 }

+ 5 - 13
pkg/progress/writer.go

@@ -66,11 +66,7 @@ func Run(ctx context.Context, pf progressFunc, out *streams.Out) error {
 }
 
 func RunWithLog(ctx context.Context, pf progressFunc, out *streams.Out, logConsumer api.LogConsumer) error {
-	dryRun, ok := ctx.Value(api.DryRunKey{}).(bool)
-	if !ok {
-		dryRun = false
-	}
-	w := NewMixedWriter(out, logConsumer, dryRun)
+	w := NewMixedWriter(out, logConsumer, false) // FIXME(ndeloof) re-implement dry-run
 	eg, _ := errgroup.WithContext(ctx)
 	eg.Go(func() error {
 		return w.Start(context.Background())
@@ -137,10 +133,6 @@ var Mode = ModeAuto
 // NewWriter returns a new multi-progress writer
 func NewWriter(ctx context.Context, out *streams.Out, progressTitle string) (Writer, error) {
 	isTerminal := out.IsTerminal()
-	dryRun, ok := ctx.Value(api.DryRunKey{}).(bool)
-	if !ok {
-		dryRun = false
-	}
 	switch Mode {
 	case ModeQuiet:
 		return quiet{}, nil
@@ -148,20 +140,20 @@ func NewWriter(ctx context.Context, out *streams.Out, progressTitle string) (Wri
 		return &jsonWriter{
 			out:    out,
 			done:   make(chan bool),
-			dryRun: dryRun,
+			dryRun: false, // FIXME(ndeloof) re-implement dry-run
 		}, nil
 	case ModeTTY:
-		return newTTYWriter(out, dryRun, progressTitle)
+		return newTTYWriter(out, false, progressTitle)
 	case ModeAuto, "":
 		if isTerminal {
-			return newTTYWriter(out, dryRun, progressTitle)
+			return newTTYWriter(out, false, progressTitle)
 		}
 		fallthrough
 	case ModePlain:
 		return &plainWriter{
 			out:    out,
 			done:   make(chan bool),
-			dryRun: dryRun,
+			dryRun: false,
 		}, nil
 	}
 	return nil, fmt.Errorf("unknown progress mode: %s", Mode)