Sfoglia il codice sorgente

Migrate CLI commands to use LoadProject API

Simplifying the codebase and eliminating duplicate backend creation.

Signed-off-by: Guillaume Lours <[email protected]>
Guillaume Lours 2 mesi fa
parent
commit
b80bb0586e

+ 7 - 1
cmd/compose/bridge.go

@@ -30,6 +30,7 @@ import (
 
 	"github.com/docker/compose/v2/cmd/formatter"
 	"github.com/docker/compose/v2/pkg/bridge"
+	"github.com/docker/compose/v2/pkg/compose"
 )
 
 func bridgeCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
@@ -62,7 +63,12 @@ func convertCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
 }
 
 func runConvert(ctx context.Context, dockerCli command.Cli, p *ProjectOptions, opts bridge.ConvertOptions) error {
-	project, _, err := p.ToProject(ctx, dockerCli, nil)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := p.ToProject(ctx, dockerCli, backend, nil)
 	if err != nil {
 		return err
 	}

+ 6 - 5
cmd/compose/build.go

@@ -150,8 +150,13 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back
 }
 
 func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts buildOptions, services []string) error {
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
+
 	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)
+	project, _, err := opts.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
@@ -166,9 +171,5 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions *Backen
 	}
 	apiBuildOptions.Attestations = true
 
-	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
-	if err != nil {
-		return err
-	}
 	return backend.Build(ctx, project, apiBuildOptions)
 }

+ 12 - 2
cmd/compose/completion.go

@@ -38,7 +38,12 @@ func noCompletion() validArgsFn {
 func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		p.Offline = true
-		project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
+		backend, err := compose.NewComposeService(dockerCli)
+		if err != nil {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+
+		project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
 		if err != nil {
 			return nil, cobra.ShellCompDirectiveNoFileComp
 		}
@@ -79,7 +84,12 @@ func completeProjectNames(dockerCli command.Cli, backendOptions *BackendOptions)
 func completeProfileNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn {
 	return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		p.Offline = true
-		project, _, err := p.ToProject(cmd.Context(), dockerCli, nil)
+		backend, err := compose.NewComposeService(dockerCli)
+		if err != nil {
+			return nil, cobra.ShellCompDirectiveNoFileComp
+		}
+
+		project, _, err := p.ToProject(cmd.Context(), dockerCli, backend, nil)
 		if err != nil {
 			return nil, cobra.ShellCompDirectiveNoFileComp
 		}

+ 35 - 52
cmd/compose/compose.go

@@ -159,12 +159,12 @@ func (o *ProjectOptions) WithProject(fn ProjectFunc, dockerCli command.Cli) func
 // WithServices creates a cobra run command from a ProjectFunc based on configured project options and selected services
 func (o *ProjectOptions) WithServices(dockerCli command.Cli, fn ProjectServicesFunc) func(cmd *cobra.Command, args []string) error {
 	return Adapt(func(ctx context.Context, args []string) error {
-		options := []cli.ProjectOptionsFn{
-			cli.WithResolvedPaths(true),
-			cli.WithoutEnvironmentResolution,
+		backend, err := compose.NewComposeService(dockerCli)
+		if err != nil {
+			return err
 		}
 
-		project, metrics, err := o.ToProject(ctx, dockerCli, args, options...)
+		project, metrics, err := o.ToProject(ctx, dockerCli, backend, args, cli.WithoutEnvironmentResolution)
 		if err != nil {
 			return err
 		}
@@ -236,7 +236,12 @@ func (o *ProjectOptions) projectOrName(ctx context.Context, dockerCli command.Cl
 	name := o.ProjectName
 	var project *types.Project
 	if len(o.ConfigPaths) > 0 || o.ProjectName == "" {
-		p, _, err := o.ToProject(ctx, dockerCli, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
+		backend, err := compose.NewComposeService(dockerCli)
+		if err != nil {
+			return nil, "", err
+		}
+
+		p, _, err := o.ToProject(ctx, dockerCli, backend, services, cli.WithDiscardEnvFile, cli.WithoutEnvironmentResolution)
 		if err != nil {
 			envProjectName := os.Getenv(ComposeProjectName)
 			if envProjectName != "" {
@@ -260,7 +265,12 @@ func (o *ProjectOptions) toProjectName(ctx context.Context, dockerCli command.Cl
 		return envProjectName, nil
 	}
 
-	project, _, err := o.ToProject(ctx, dockerCli, nil)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return "", err
+	}
+
+	project, _, err := o.ToProject(ctx, dockerCli, backend, nil)
 	if err != nil {
 		return "", err
 	}
@@ -285,19 +295,14 @@ func (o *ProjectOptions) ToModel(ctx context.Context, dockerCli command.Cli, ser
 	return options.LoadModel(ctx)
 }
 
-func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) { //nolint:gocyclo
+// ToProject loads a Compose project using the LoadProject API.
+// Accepts optional cli.ProjectOptionsFn to control loader behavior.
+func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string, po ...cli.ProjectOptionsFn) (*types.Project, tracing.Metrics, error) {
 	var metrics tracing.Metrics
 	remotes := o.remoteLoaders(dockerCli)
-	for _, r := range remotes {
-		po = append(po, cli.WithResourceLoader(r))
-	}
-
-	options, err := o.toProjectOptions(po...)
-	if err != nil {
-		return nil, metrics, err
-	}
 
-	options.WithListeners(func(event string, metadata map[string]any) {
+	// Setup metrics listener to collect project data
+	metricsListener := func(event string, metadata map[string]any) {
 		switch event {
 		case "extends":
 			metrics.CountExtends++
@@ -318,50 +323,28 @@ func (o *ProjectOptions) ToProject(ctx context.Context, dockerCli command.Cli, s
 				}
 			}
 		}
-	})
-
-	if o.Compatibility || utils.StringToBool(options.Environment[ComposeCompatibility]) {
-		api.Separator = "_"
-	}
-
-	project, err := options.LoadProject(ctx)
-	if err != nil {
-		return nil, metrics, err
 	}
 
-	if project.Name == "" {
-		return nil, metrics, errors.New("project name can't be empty. Use `--project-name` to set a valid name")
+	loadOpts := api.ProjectLoadOptions{
+		ProjectName:       o.ProjectName,
+		ConfigPaths:       o.ConfigPaths,
+		WorkingDir:        o.ProjectDir,
+		EnvFiles:          o.EnvFiles,
+		Profiles:          o.Profiles,
+		Services:          services,
+		Offline:           o.Offline,
+		All:               o.All,
+		Compatibility:     o.Compatibility,
+		ProjectOptionsFns: po,
+		LoadListeners:     []api.LoadListener{metricsListener},
 	}
 
-	project, err = project.WithServicesEnabled(services...)
+	project, err := backend.LoadProject(ctx, loadOpts)
 	if err != nil {
 		return nil, metrics, err
 	}
 
-	for name, s := range project.Services {
-		s.CustomLabels = map[string]string{
-			api.ProjectLabel:     project.Name,
-			api.ServiceLabel:     name,
-			api.VersionLabel:     api.ComposeVersion,
-			api.WorkingDirLabel:  project.WorkingDir,
-			api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
-			api.OneoffLabel:      "False", // default, will be overridden by `run` command
-		}
-		if len(o.EnvFiles) != 0 {
-			s.CustomLabels[api.EnvironmentFileLabel] = strings.Join(o.EnvFiles, ",")
-		}
-		project.Services[name] = s
-	}
-
-	project, err = project.WithSelectedServices(services)
-	if err != nil {
-		return nil, tracing.Metrics{}, err
-	}
-
-	if !o.All {
-		project = project.WithoutUnnecessaryResources()
-	}
-	return project, metrics, err
+	return project, metrics, nil
 }
 
 func (o *ProjectOptions) remoteLoaders(dockerCli command.Cli) []loader.ResourceLoader {

+ 66 - 15
cmd/compose/config.go

@@ -61,19 +61,19 @@ type configOptions struct {
 	lockImageDigests    bool
 }
 
-func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (*types.Project, error) {
-	po = append(po, o.ToProjectOptions()...)
-	project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, services, po...)
+func (o *configOptions) ToProject(ctx context.Context, dockerCli command.Cli, backend api.Compose, services []string) (*types.Project, error) {
+	project, _, err := o.ProjectOptions.ToProject(ctx, dockerCli, backend, services, o.toProjectOptionsFns()...)
 	return project, err
 }
 
 func (o *configOptions) ToModel(ctx context.Context, dockerCli command.Cli, services []string, po ...cli.ProjectOptionsFn) (map[string]any, error) {
-	po = append(po, o.ToProjectOptions()...)
+	po = append(po, o.toProjectOptionsFns()...)
 	return o.ProjectOptions.ToModel(ctx, dockerCli, services, po...)
 }
 
-func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
-	return []cli.ProjectOptionsFn{
+// toProjectOptionsFns converts config options to cli.ProjectOptionsFn
+func (o *configOptions) toProjectOptionsFns() []cli.ProjectOptionsFn {
+	fns := []cli.ProjectOptionsFn{
 		cli.WithInterpolation(!o.noInterpolate),
 		cli.WithResolvedPaths(!o.noResolvePath),
 		cli.WithNormalization(!o.noNormalize),
@@ -81,6 +81,10 @@ func (o *configOptions) ToProjectOptions() []cli.ProjectOptionsFn {
 		cli.WithDefaultProfiles(o.Profiles...),
 		cli.WithDiscardEnvFile,
 	}
+	if o.noResolveEnv {
+		fns = append(fns, cli.WithoutEnvironmentResolution)
+	}
+	return fns
 }
 
 func configCommand(p *ProjectOptions, dockerCli command.Cli) *cobra.Command {
@@ -197,7 +201,12 @@ func runConfig(ctx context.Context, dockerCli command.Cli, opts configOptions, s
 }
 
 func runConfigInterpolate(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) ([]byte, error) {
-	project, err := opts.ToProject(ctx, dockerCli, services)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return nil, err
+	}
+
+	project, err := opts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return nil, err
 	}
@@ -353,7 +362,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions)
 		return nil
 	}
 
-	project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
@@ -366,7 +380,12 @@ func runServices(ctx context.Context, dockerCli command.Cli, opts configOptions)
 }
 
 func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
-	project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
@@ -377,7 +396,12 @@ func runVolumes(ctx context.Context, dockerCli command.Cli, opts configOptions)
 }
 
 func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
-	project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
@@ -388,7 +412,12 @@ func runNetworks(ctx context.Context, dockerCli command.Cli, opts configOptions)
 }
 
 func runModels(ctx context.Context, dockerCli command.Cli, opts configOptions) error {
-	project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
@@ -405,7 +434,13 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
 	if opts.hash != "*" {
 		services = append(services, strings.Split(opts.hash, ",")...)
 	}
-	project, err := opts.ToProject(ctx, dockerCli, nil, cli.WithoutEnvironmentResolution)
+
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := opts.ProjectOptions.ToProject(ctx, dockerCli, backend, nil, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
@@ -440,7 +475,13 @@ func runHash(ctx context.Context, dockerCli command.Cli, opts configOptions) err
 
 func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
 	set := map[string]struct{}{}
-	project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
+
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, err := opts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return err
 	}
@@ -461,7 +502,12 @@ func runProfiles(ctx context.Context, dockerCli command.Cli, opts configOptions,
 }
 
 func runConfigImages(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
-	project, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, err := opts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return err
 	}
@@ -498,7 +544,12 @@ func runVariables(ctx context.Context, dockerCli command.Cli, opts configOptions
 }
 
 func runEnvironment(ctx context.Context, dockerCli command.Cli, opts configOptions, services []string) error {
-	project, err := opts.ToProject(ctx, dockerCli, services)
+	backend, err := compose.NewComposeService(dockerCli)
+	if err != nil {
+		return err
+	}
+
+	project, err := opts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return err
 	}

+ 10 - 8
cmd/compose/publish.go

@@ -69,7 +69,16 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Ba
 }
 
 func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts publishOptions, repository string) error {
-	project, metrics, err := opts.ToProject(ctx, dockerCli, nil)
+	if opts.assumeYes {
+		backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
+	}
+
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
+
+	project, metrics, err := opts.ToProject(ctx, dockerCli, backend, nil)
 	if err != nil {
 		return err
 	}
@@ -78,13 +87,6 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions *Back
 		return errors.New("cannot publish compose file with local includes")
 	}
 
-	if opts.assumeYes {
-		backendOptions.Options = append(backendOptions.Options, compose.WithPrompt(compose.AlwaysOkPrompt()))
-	}
-	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,

+ 4 - 3
cmd/compose/pull.go

@@ -99,20 +99,21 @@ func (opts pullOptions) apply(project *types.Project, services []string) (*types
 }
 
 func runPull(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pullOptions, services []string) error {
-	project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution)
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
 	if err != nil {
 		return err
 	}
 
-	project, err = opts.apply(project, services)
+	project, _, err := opts.ToProject(ctx, dockerCli, backend, services, cli.WithoutEnvironmentResolution)
 	if err != nil {
 		return err
 	}
 
-	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	project, err = opts.apply(project, services)
 	if err != nil {
 		return err
 	}
+
 	return backend.Pull(ctx, project, api.PullOptions{
 		Quiet:           opts.quiet,
 		IgnoreFailures:  opts.ignorePullFailures,

+ 6 - 5
cmd/compose/push.go

@@ -55,7 +55,12 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backe
 }
 
 func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts pushOptions, services []string) error {
-	project, _, err := opts.ToProject(ctx, dockerCli, services)
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return err
 	}
@@ -67,10 +72,6 @@ func runPush(ctx context.Context, dockerCli command.Cli, backendOptions *Backend
 		}
 	}
 
-	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,

+ 10 - 9
cmd/compose/run.go

@@ -22,6 +22,7 @@ import (
 	"os"
 	"strings"
 
+	composecli "github.com/compose-spec/compose-go/v2/cli"
 	"github.com/compose-spec/compose-go/v2/dotenv"
 	"github.com/compose-spec/compose-go/v2/format"
 	"github.com/docker/compose/v2/pkg/compose"
@@ -29,7 +30,6 @@ import (
 	xprogress "github.com/moby/buildkit/util/progress/progressui"
 	"github.com/sirupsen/logrus"
 
-	cgo "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/cli/opts"
@@ -143,7 +143,7 @@ func (options runOptions) getEnvironment(resolve func(string) (string, bool)) (t
 	return environment, nil
 }
 
-func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command {
+func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *BackendOptions) *cobra.Command { //nolint:gocyclo
 	options := runOptions{
 		composeOptions: &composeOptions{
 			ProjectOptions: p,
@@ -204,7 +204,12 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
 			return nil
 		}),
 		RunE: Adapt(func(ctx context.Context, args []string) error {
-			project, _, err := p.ToProject(ctx, dockerCli, []string{options.Service}, cgo.WithResolvedPaths(true), cgo.WithoutEnvironmentResolution)
+			backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+			if err != nil {
+				return err
+			}
+
+			project, _, err := p.ToProject(ctx, dockerCli, backend, []string{options.Service}, composecli.WithoutEnvironmentResolution)
 			if err != nil {
 				return err
 			}
@@ -219,7 +224,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
 			}
 
 			options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans])
-			return runRun(ctx, backendOptions, project, options, createOpts, buildOpts, dockerCli)
+			return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli)
 		}),
 		ValidArgsFunction: completeServiceNames(dockerCli, p),
 	}
@@ -267,7 +272,7 @@ func normalizeRunFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
 	return pflag.NormalizedName(name)
 }
 
-func runRun(ctx context.Context, backendOptions *BackendOptions, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
+func runRun(ctx context.Context, backend api.Compose, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error {
 	project, err := options.apply(project)
 	if err != nil {
 		return err
@@ -339,10 +344,6 @@ func runRun(ctx context.Context, backendOptions *BackendOptions, project *types.
 		}
 	}
 
-	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
-	if err != nil {
-		return err
-	}
 	exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts)
 	if exitCode != 0 {
 		errMsg := ""

+ 6 - 5
cmd/compose/scale.go

@@ -60,8 +60,13 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back
 }
 
 func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, opts scaleOptions, serviceReplicaTuples map[string]int) error {
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
+
 	services := slices.Sorted(maps.Keys(serviceReplicaTuples))
-	project, _, err := opts.ToProject(ctx, dockerCli, services)
+	project, _, err := opts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return err
 	}
@@ -81,10 +86,6 @@ func runScale(ctx context.Context, dockerCli command.Cli, backendOptions *Backen
 		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})
 }
 

+ 5 - 3
cmd/compose/viz.go

@@ -66,16 +66,18 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Backen
 
 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)
+
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
 	if err != nil {
 		return err
 	}
 
-	// build graph
-	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	project, _, err := opts.ToProject(ctx, dockerCli, backend, nil)
 	if err != nil {
 		return err
 	}
+
+	// build graph
 	graphStr, _ := backend.Viz(ctx, project, api.VizOptions{
 		IncludeNetworks:  opts.includeNetworks,
 		IncludePorts:     opts.includePorts,

+ 6 - 9
cmd/compose/watch.go

@@ -66,7 +66,12 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions *Back
 }
 
 func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *BackendOptions, watchOpts watchOptions, buildOpts buildOptions, services []string) error {
-	project, _, err := watchOpts.ToProject(ctx, dockerCli, services)
+	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
+	if err != nil {
+		return err
+	}
+
+	project, _, err := watchOpts.ToProject(ctx, dockerCli, backend, services)
 	if err != nil {
 		return err
 	}
@@ -112,19 +117,11 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions *Backen
 				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
 		}
 	}
 
-	backend, err := compose.NewComposeService(dockerCli, backendOptions.Options...)
-	if err != nil {
-		return err
-	}
 	outStream, errStream, _ := backend.GetConfiguredStreams()
 	consumer := formatter.NewLogConsumer(ctx, outStream, errStream, false, false, false)
 	return backend.Watch(ctx, project, api.WatchOptions{