diff --git a/cmd/compose/alpha.go b/cmd/compose/alpha.go index 74636393e18..33bf4c4c483 100644 --- a/cmd/compose/alpha.go +++ b/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,12 @@ package compose 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" ) // alphaCommand groups all experimental subcommands -func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { cmd := &cobra.Command{ Short: "Experimental commands", Use: "alpha [COMMAND]", @@ -31,9 +30,9 @@ func alphaCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) }, } 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 } diff --git a/cmd/compose/attach.go b/cmd/compose/attach.go index a4504a0abad..94143a62146 100644 --- a/cmd/compose/attach.go +++ b/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.Service) *cobra.Command { +func attachCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := attachOpts{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -50,7 +51,7 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service 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,12 +64,17 @@ func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return runCmd } -func runAttach(ctx context.Context, dockerCli command.Cli, backend api.Service, opts attachOpts) error { +func runAttach(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts attachOpts) error { projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + attachOpts := api.AttachOptions{ Service: opts.service, Index: opts.index, diff --git a/cmd/compose/build.go b/cmd/compose/build.go index 041c7b42dfa..72c8e32417a 100644 --- a/cmd/compose/build.go +++ b/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.Service) *cobra.Command { +func buildCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := buildOptions{ ProjectOptions: p, } @@ -115,7 +116,7 @@ func buildCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return cmd } -func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, opts buildOptions, services []string) error { +func runBuild(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, 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,10 @@ func runBuild(ctx context.Context, dockerCli command.Cli, backend api.Service, o } apiBuildOptions.Attestations = true + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Build(ctx, project, apiBuildOptions) } diff --git a/cmd/compose/commit.go b/cmd/compose/commit.go index 9b07981a054..cc7327a5dce 100644 --- a/cmd/compose/commit.go +++ b/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.Service) *cobra.Command { +func commitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { options := commitOptions{ ProjectOptions: p, } @@ -56,7 +57,7 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service 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,17 @@ func commitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return cmd } -func runCommit(ctx context.Context, dockerCli command.Cli, backend api.Service, options commitOptions) error { +func runCommit(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, options commitOptions) error { projectName, err := options.toProjectName(ctx, dockerCli) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Commit(ctx, projectName, api.CommitOptions{ Service: options.service, Reference: options.reference, diff --git a/cmd/compose/completion.go b/cmd/compose/completion.go index dd23ed0747d..1d095eb48f6 100644 --- a/cmd/compose/completion.go +++ b/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,12 @@ func completeServiceNames(dockerCli command.Cli, p *ProjectOptions) validArgsFn } } -func completeProjectNames(backend api.Service) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func completeProjectNames(cli command.Cli) 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(cli) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } list, err := backend.List(cmd.Context(), api.ListOptions{ All: true, }) diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 2bba69ad976..184ab66c64d 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -40,8 +40,10 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/pkg/kvfile" "github.com/docker/compose/v2/cmd/formatter" + "github.com/docker/compose/v2/cmd/prompt" "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" @@ -416,7 +418,7 @@ func RunningAsStandalone() bool { } // RootCommand returns the compose command with its child commands -func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { //nolint:gocyclo +func RootCommand(dockerCli command.Cli) *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 @@ -436,6 +438,11 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // parallel int dryRun bool ) + + backendOptions := []compose.Option{ + compose.WithPrompt(prompt.NewPrompt(dockerCli.In(), dockerCli.Out()).Confirm), + } + c := &cobra.Command{ Short: "Docker Compose", Long: "Define and run multi-container applications with Docker", @@ -560,64 +567,64 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // } if parallel > 0 { logrus.Debugf("Limiting max concurrency to %d jobs", parallel) - backend.MaxConcurrency(parallel) + backendOptions = append(backendOptions, compose.WithMaxConcurrency(parallel)) } - // dry run detection - ctx, err = backend.DryRunMode(ctx, dryRun) - if err != nil { - return err + if dryRun { + backendOptions = append(backendOptions, compose.WithDryRun) + // FIXME to be removed after progress is moved under cmd + ctx = context.WithValue(ctx, api.DryRunKey{}, dryRun) + cmd.SetContext(ctx) } - 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), ) c.RegisterFlagCompletionFunc( //nolint:errcheck "project-directory", diff --git a/cmd/compose/cp.go b/cmd/compose/cp.go index bd6281fc830..14625892216 100644 --- a/cmd/compose/cp.go +++ b/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.Service) *cobra.Command { +func copyCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := copyOptions{ ProjectOptions: p, } @@ -59,7 +60,7 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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,17 @@ func copyCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) return copyCmd } -func runCopy(ctx context.Context, dockerCli command.Cli, backend api.Service, opts copyOptions) error { +func runCopy(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts copyOptions) error { name, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Copy(ctx, name, api.CopyOptions{ Source: opts.source, Destination: opts.destination, diff --git a/cmd/compose/create.go b/cmd/compose/create.go index a96d63fe1e5..bfda0aa7a05 100644 --- a/cmd/compose/create.go +++ b/cmd/compose/create.go @@ -26,6 +26,8 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" + "github.com/docker/compose/v2/cmd/prompt" + "github.com/docker/compose/v2/pkg/compose" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -51,7 +53,7 @@ type createOptions struct { AssumeYes bool } -func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func createCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := createOptions{} buildOpts := buildOptions{ ProjectOptions: p, @@ -70,7 +72,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service 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 +97,7 @@ func createCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return cmd } -func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error { +func runCreate(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, createOpts createOptions, buildOpts buildOptions, project *types.Project, services []string) error { if err := createOpts.Apply(project); err != nil { return err } @@ -109,6 +111,15 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp build = &bo } + if createOpts.AssumeYes { + backendOptions = append(backendOptions, compose.WithPrompt(prompt.Yes)) + } + + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Create(ctx, project, api.CreateOptions{ Build: build, Services: services, @@ -119,7 +130,6 @@ func runCreate(ctx context.Context, _ command.Cli, backend api.Service, createOp Inherit: !createOpts.noInherit, Timeout: createOpts.GetTimeout(), QuietPull: createOpts.quietPull, - AssumeYes: createOpts.AssumeYes, }) } diff --git a/cmd/compose/create_test.go b/cmd/compose/create_test.go deleted file mode 100644 index e4677b973b1..00000000000 --- a/cmd/compose/create_test.go +++ /dev/null @@ -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) -} diff --git a/cmd/compose/down.go b/cmd/compose/down.go index d3080ed2df5..40177d8f5f7 100644 --- a/cmd/compose/down.go +++ b/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.Service) *cobra.Command { +func downCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := downOptions{ ProjectOptions: p, } @@ -57,7 +58,7 @@ func downCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return downCmd } -func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, opts downOptions, services []string) error { +func runDown(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts downOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err @@ -88,6 +89,12 @@ func runDown(ctx context.Context, dockerCli command.Cli, backend api.Service, op timeoutValue := time.Duration(opts.timeout) * time.Second timeout = &timeoutValue } + + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Down(ctx, name, api.DownOptions{ RemoveOrphans: opts.removeOrphans, Project: project, diff --git a/cmd/compose/events.go b/cmd/compose/events.go index 181048c40f5..eff0a51f0bd 100644 --- a/cmd/compose/events.go +++ b/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.Service) *cobra.Command { +func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := eventsOpts{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -44,7 +45,7 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service 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,17 @@ func eventsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return cmd } -func runEvents(ctx context.Context, dockerCli command.Cli, backend api.Service, opts eventsOpts, services []string) error { +func runEvents(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts eventsOpts, services []string) error { name, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Events(ctx, name, api.EventsOptions{ Services: services, Since: opts.since, diff --git a/cmd/compose/exec.go b/cmd/compose/exec.go index a978b65ff3f..18fc39361f4 100644 --- a/cmd/compose/exec.go +++ b/cmd/compose/exec.go @@ -48,7 +48,7 @@ type execOpts struct { interactive bool } -func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func execCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := execOpts{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -64,7 +64,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return runCmd } -func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, opts execOpts) error { +func runExec(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts execOpts) error { projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err @@ -126,6 +126,11 @@ func runExec(ctx context.Context, dockerCli command.Cli, backend api.Service, op Interactive: opts.interactive, } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + exitCode, err := backend.Exec(ctx, projectName, execOpts) if exitCode != 0 { errMsg := fmt.Sprintf("exit status %d", exitCode) diff --git a/cmd/compose/export.go b/cmd/compose/export.go index 8ad08b7d2ae..f203670d313 100644 --- a/cmd/compose/export.go +++ b/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.Service) *cobra.Command { +func exportCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { options := exportOptions{ ProjectOptions: p, } @@ -46,7 +47,7 @@ func exportCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service 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.Service return cmd } -func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, options exportOptions) error { +func runExport(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, options exportOptions) error { projectName, err := options.toProjectName(ctx, dockerCli) if err != nil { return err @@ -70,5 +71,10 @@ func runExport(ctx context.Context, dockerCli command.Cli, backend api.Service, Output: options.output, } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Export(ctx, projectName, exportOptions) } diff --git a/cmd/compose/generate.go b/cmd/compose/generate.go index a0b32057d34..1c5cd7fe7f7 100644 --- a/cmd/compose/generate.go +++ b/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.Service) *cobra.Command { +func generateCommand(p *ProjectOptions, cli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := generateOptions{ ProjectOptions: p, } @@ -42,7 +44,7 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { - return runGenerate(ctx, backend, opts, args) + return runGenerate(ctx, cli, backendOptions, opts, args) }), } @@ -52,11 +54,17 @@ func generateCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return cmd } -func runGenerate(ctx context.Context, backend api.Service, opts generateOptions, containers []string) error { +func runGenerate(ctx context.Context, cli command.Cli, backendOptions []compose.Option, 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(cli, backendOptions...) + if err != nil { + return err + } + project, err := backend.Generate(ctx, api.GenerateOptions{ Containers: containers, ProjectName: opts.ProjectName, diff --git a/cmd/compose/images.go b/cmd/compose/images.go index 4f305e49195..0c31568ebd2 100644 --- a/cmd/compose/images.go +++ b/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.Service) *cobra.Command { +func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := imageOptions{ ProjectOptions: p, } @@ -49,7 +50,7 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service 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,17 @@ func imagesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service return imgCmd } -func runImages(ctx context.Context, dockerCli command.Cli, backend api.Service, opts imageOptions, services []string) error { +func runImages(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts imageOptions, services []string) error { projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + images, err := backend.Images(ctx, projectName, api.ImagesOptions{ Services: services, }) diff --git a/cmd/compose/kill.go b/cmd/compose/kill.go index c0faa75c60e..2285e8f673e 100644 --- a/cmd/compose/kill.go +++ b/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.Service) *cobra.Command { +func killCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := killOptions{ ProjectOptions: p, } @@ -41,7 +42,7 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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,17 @@ func killCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) return cmd } -func runKill(ctx context.Context, dockerCli command.Cli, backend api.Service, opts killOptions, services []string) error { +func runKill(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts killOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Kill(ctx, name, api.KillOptions{ RemoveOrphans: opts.removeOrphans, Project: project, diff --git a/cmd/compose/list.go b/cmd/compose/list.go index 38fb2ce8c54..f5efdd57a81 100644 --- a/cmd/compose/list.go +++ b/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.Service) *cobra.Command { +func listCommand(dockerCli command.Cli, backendOptions []compose.Option) *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,18 @@ var acceptedListFilters = map[string]bool{ "name": true, } -func runList(ctx context.Context, dockerCli command.Cli, backend api.Service, lsOpts lsOptions) error { +func runList(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, lsOpts lsOptions) error { filters := lsOpts.Filter.Value() err := filters.Validate(acceptedListFilters) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + stackList, err := backend.List(ctx, api.ListOptions{All: lsOpts.All}) if err != nil { return err diff --git a/cmd/compose/logs.go b/cmd/compose/logs.go index 7661b02aed6..abc641a3d8c 100644 --- a/cmd/compose/logs.go +++ b/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.Service) *cobra.Command { +func logsCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := logsOptions{ ProjectOptions: p, } @@ -48,7 +49,7 @@ func logsCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return logsCmd } -func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, opts logsOptions, services []string) error { +func runLogs(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts logsOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err @@ -85,6 +86,11 @@ func runLogs(ctx context.Context, dockerCli command.Cli, backend api.Service, op } } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), !opts.noColor, !opts.noPrefix, false) return backend.Logs(ctx, name, consumer, api.LogOptions{ Project: project, diff --git a/cmd/compose/options.go b/cmd/compose/options.go index 9e879fb0716..2c864c58a96 100644 --- a/cmd/compose/options.go +++ b/cmd/compose/options.go @@ -30,9 +30,9 @@ import ( "github.com/compose-spec/compose-go/v2/template" "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" + "github.com/docker/compose/v2/cmd/prompt" "github.com/docker/compose/v2/internal/tracing" ui "github.com/docker/compose/v2/pkg/progress" - "github.com/docker/compose/v2/pkg/prompt" ) func applyPlatforms(project *types.Project, buildForSinglePlatform bool) error { diff --git a/cmd/compose/pause.go b/cmd/compose/pause.go index 6f34577192e..4d3eff9406a 100644 --- a/cmd/compose/pause.go +++ b/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.Service) *cobra.Command { +func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := pauseOptions{ ProjectOptions: p, } @@ -37,19 +38,24 @@ func pauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service, opts pauseOptions, services []string) error { +func runPause(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts pauseOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Pause(ctx, name, api.PauseOptions{ Services: services, Project: project, @@ -60,7 +66,7 @@ type unpauseOptions struct { *ProjectOptions } -func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := unpauseOptions{ ProjectOptions: p, } @@ -68,19 +74,24 @@ func unpauseCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic 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.Service, opts unpauseOptions, services []string) error { +func runUnPause(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts unpauseOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.UnPause(ctx, name, api.PauseOptions{ Services: services, Project: project, diff --git a/cmd/compose/port.go b/cmd/compose/port.go index 9d49a226202..46508220861 100644 --- a/cmd/compose/port.go +++ b/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.Service) *cobra.Command { +func portCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := portOptions{ ProjectOptions: p, } @@ -53,7 +54,7 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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,17 @@ func portCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) return cmd } -func runPort(ctx context.Context, dockerCli command.Cli, backend api.Service, opts portOptions, service string) error { +func runPort(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts portOptions, service string) error { projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } + + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + ip, port, err := backend.Port(ctx, projectName, service, opts.port, api.PortOptions{ Protocol: opts.protocol, Index: opts.index, diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go index f393b2c50fb..1abaf92efaa 100644 --- a/cmd/compose/ps.go +++ b/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.Service) *cobra.Command { +func psCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := psOptions{ ProjectOptions: p, } @@ -75,7 +76,7 @@ func psCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *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.Service) *c return psCmd } -func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, opts psOptions) error { +func runPs(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, services []string, opts psOptions) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err @@ -111,6 +112,11 @@ func runPs(ctx context.Context, dockerCli command.Cli, backend api.Service, serv } } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + containers, err := backend.Ps(ctx, name, api.PsOptions{ Project: project, All: opts.All || len(opts.Status) != 0, diff --git a/cmd/compose/ps_test.go b/cmd/compose/ps_test.go deleted file mode 100644 index 79f1b49ac0e..00000000000 --- a/cmd/compose/ps_test.go +++ /dev/null @@ -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") -} diff --git a/cmd/compose/publish.go b/cmd/compose/publish.go index 174c8fa8695..a753497494c 100644 --- a/cmd/compose/publish.go +++ b/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.Service) *cobra.Command { +func publishCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := publishOptions{ ProjectOptions: p, } @@ -45,7 +46,7 @@ func publishCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic 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.Servic return cmd } -func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, opts publishOptions, repository string) error { +func runPublish(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts publishOptions, repository string) error { project, metrics, err := opts.ToProject(ctx, dockerCli, nil) if err != nil { return err @@ -77,6 +78,11 @@ func runPublish(ctx context.Context, dockerCli command.Cli, backend api.Service, return errors.New("cannot publish compose file with local includes") } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Publish(ctx, project, repository, api.PublishOptions{ ResolveImageDigests: opts.resolveImageDigests || opts.app, Application: opts.app, diff --git a/cmd/compose/pull.go b/cmd/compose/pull.go index 4951fa61c19..1953c466a1b 100644 --- a/cmd/compose/pull.go +++ b/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.Service) *cobra.Command { +func pullCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := pullOptions{ ProjectOptions: p, } @@ -59,7 +60,7 @@ func pullCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service, opts pullOptions, services []string) error { +func runPull(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts pullOptions, services []string) error { project, _, err := opts.ToProject(ctx, dockerCli, services, cli.WithoutEnvironmentResolution) if err != nil { return err @@ -108,6 +109,11 @@ func runPull(ctx context.Context, dockerCli command.Cli, backend api.Service, op return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Pull(ctx, project, api.PullOptions{ Quiet: opts.quiet, IgnoreFailures: opts.ignorePullFailures, diff --git a/cmd/compose/push.go b/cmd/compose/push.go index 177f9f2ec7c..e29c440f4d1 100644 --- a/cmd/compose/push.go +++ b/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.Service) *cobra.Command { +func pushCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := pushOptions{ ProjectOptions: p, } @@ -42,7 +43,7 @@ func pushCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return pushCmd } -func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, opts pushOptions, services []string) error { +func runPush(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts pushOptions, services []string) error { project, _, err := opts.ToProject(ctx, dockerCli, services) if err != nil { return err @@ -66,6 +67,11 @@ func runPush(ctx context.Context, dockerCli command.Cli, backend api.Service, op } } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Push(ctx, project, api.PushOptions{ IgnoreFailures: opts.Ignorefailures, Quiet: opts.Quiet, diff --git a/cmd/compose/remove.go b/cmd/compose/remove.go index adcd3663c3b..1d996980e57 100644 --- a/cmd/compose/remove.go +++ b/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.Service) *cobra.Command { +func removeCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *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,17 @@ Any data which is not in a volume will be lost.`, return cmd } -func runRemove(ctx context.Context, dockerCli command.Cli, backend api.Service, opts removeOptions, services []string) error { +func runRemove(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts removeOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Remove(ctx, name, api.RemoveOptions{ Services: services, Force: opts.force, diff --git a/cmd/compose/restart.go b/cmd/compose/restart.go index 718e15196f0..1e430c93d39 100644 --- a/cmd/compose/restart.go +++ b/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.Service) *cobra.Command { +func restartCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := restartOptions{ ProjectOptions: p, } @@ -44,7 +45,7 @@ func restartCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic 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.Servic return restartCmd } -func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, opts restartOptions, services []string) error { +func runRestart(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts restartOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli) if err != nil { return err @@ -74,6 +75,11 @@ func runRestart(ctx context.Context, dockerCli command.Cli, backend api.Service, timeout = &timeoutValue } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Restart(ctx, name, api.RestartOptions{ Timeout: timeout, Services: services, diff --git a/cmd/compose/run.go b/cmd/compose/run.go index 2c6362ef183..586f9284e67 100644 --- a/cmd/compose/run.go +++ b/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.Service) *cobra.Command { +func runCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { options := runOptions{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -218,7 +219,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * } options.ignoreOrphans = utils.StringToBool(project.Environment[ComposeIgnoreOrphans]) - return runRun(ctx, backend, project, options, createOpts, buildOpts, dockerCli) + return runRun(ctx, dockerCli, backendOptions, project, options, createOpts, buildOpts) }), 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.Service, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions, dockerCli command.Cli) error { +func runRun(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, project *types.Project, options runOptions, createOpts createOptions, buildOpts buildOptions) error { project, err := options.apply(project) if err != nil { return err @@ -338,6 +339,11 @@ func runRun(ctx context.Context, backend api.Service, project *types.Project, op } } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + exitCode, err := backend.RunOneOffContainer(ctx, project, runOpts) if exitCode != 0 { errMsg := "" diff --git a/cmd/compose/scale.go b/cmd/compose/scale.go index afa1f8cb232..8c48b1de9f8 100644 --- a/cmd/compose/scale.go +++ b/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.Service) *cobra.Command { +func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := scaleOptions{ ProjectOptions: p, } @@ -48,7 +49,7 @@ func scaleCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return scaleCmd } -func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, opts scaleOptions, serviceReplicaTuples map[string]int) error { +func runScale(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, 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,11 @@ func runScale(ctx context.Context, dockerCli command.Cli, backend api.Service, o project.Services[key] = service } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Scale(ctx, project, api.ScaleOptions{Services: services}) } diff --git a/cmd/compose/start.go b/cmd/compose/start.go index 6bde4b104bf..c1bda028e54 100644 --- a/cmd/compose/start.go +++ b/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.Service) *cobra.Command { +func startCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := startOptions{ ProjectOptions: p, } @@ -36,19 +37,24 @@ func startCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service, opts startOptions, services []string) error { +func runStart(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts startOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Start(ctx, name, api.StartOptions{ AttachTo: services, Project: project, diff --git a/cmd/compose/stop.go b/cmd/compose/stop.go index d06cf6f2290..f116866c45b 100644 --- a/cmd/compose/stop.go +++ b/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.Service) *cobra.Command { +func stopCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := stopOptions{ ProjectOptions: p, } @@ -43,7 +44,7 @@ func stopCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return cmd } -func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts stopOptions, services []string) error { +func runStop(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts stopOptions, services []string) error { project, name, err := opts.projectOrName(ctx, dockerCli, services...) if err != nil { return err @@ -64,6 +65,12 @@ func runStop(ctx context.Context, dockerCli command.Cli, backend api.Service, op timeoutValue := time.Duration(opts.timeout) * time.Second timeout = &timeoutValue } + + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + return backend.Stop(ctx, name, api.StopOptions{ Timeout: timeout, Services: services, diff --git a/cmd/compose/top.go b/cmd/compose/top.go index a4cbc0f403b..9b89d26a87f 100644 --- a/cmd/compose/top.go +++ b/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.Service) *cobra.Command { +func topCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := topOptions{ ProjectOptions: p, } @@ -42,7 +43,7 @@ func topCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * 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,17 @@ type ( topEntries map[string]string ) -func runTop(ctx context.Context, dockerCli command.Cli, backend api.Service, opts topOptions, services []string) error { +func runTop(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts topOptions, services []string) error { projectName, err := opts.toProjectName(ctx, dockerCli) if err != nil { return err } + + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + containers, err := backend.Top(ctx, projectName, services) if err != nil { return err diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 0c8066d05b2..2bc60facfde 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -26,6 +26,8 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/docker/cli/cli/command" + "github.com/docker/compose/v2/cmd/prompt" + "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 +111,7 @@ func (opts upOptions) OnExit() api.Cascade { } } -func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func upCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { up := upOptions{} create := createOptions{} build := buildOptions{ProjectOptions: p} @@ -140,7 +142,7 @@ func upCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *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), } @@ -225,21 +227,21 @@ func validateFlags(up *upOptions, create *createOptions) error { } //nolint:gocyclo -func runUp( - ctx context.Context, - dockerCli command.Cli, - backend api.Service, - createOptions createOptions, - upOptions upOptions, - buildOptions buildOptions, - project *types.Project, - services []string, -) error { +func runUp(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, createOptions createOptions, upOptions upOptions, buildOptions buildOptions, project *types.Project, services []string, ) error { if err := checksForRemoteStack(ctx, dockerCli, project, buildOptions, createOptions.AssumeYes, []string{}); err != nil { return err } - err := createOptions.Apply(project) + if createOptions.AssumeYes { + backendOptions = append(backendOptions, compose.WithPrompt(prompt.Yes)) + } + + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + + err = createOptions.Apply(project) if err != nil { return err } @@ -277,7 +279,6 @@ func runUp( Inherit: !createOptions.noInherit, Timeout: createOptions.GetTimeout(), QuietPull: createOptions.quietPull, - AssumeYes: createOptions.AssumeYes, } if upOptions.noStart { diff --git a/cmd/compose/viz.go b/cmd/compose/viz.go index d97504e3825..f53d2e4d64f 100644 --- a/cmd/compose/viz.go +++ b/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.Service) *cobra.Command { +func vizCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := vizOptions{ ProjectOptions: p, } @@ -51,7 +52,7 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * return err }), RunE: Adapt(func(ctx context.Context, args []string) error { - return runViz(ctx, dockerCli, backend, &opts) + return runViz(ctx, dockerCli, backendOptions, &opts) }), } @@ -63,13 +64,18 @@ func vizCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * return cmd } -func runViz(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *vizOptions) error { +func runViz(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts *vizOptions) error { _, _ = fmt.Fprintln(os.Stderr, "viz command is EXPERIMENTAL") project, _, err := opts.ToProject(ctx, dockerCli, nil) if err != nil { return err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + // build graph graphStr, _ := backend.Viz(ctx, project, api.VizOptions{ IncludeNetworks: opts.includeNetworks, diff --git a/cmd/compose/volumes.go b/cmd/compose/volumes.go index 456f586fcd8..02a24dd5efe 100644 --- a/cmd/compose/volumes.go +++ b/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.Service) *cobra.Command { +func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { options := volumesOptions{ ProjectOptions: p, } @@ -43,7 +44,7 @@ func volumesCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Servic 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.Servic return cmd } -func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, services []string, options volumesOptions) error { +func runVol(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, services []string, options volumesOptions) error { project, name, err := options.projectOrName(ctx, dockerCli, services...) if err != nil { return err @@ -69,6 +70,11 @@ func runVol(ctx context.Context, dockerCli command.Cli, backend api.Service, ser } } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + volumes, err := backend.Volumes(ctx, name, api.VolumesOptions{ Services: services, }) diff --git a/cmd/compose/wait.go b/cmd/compose/wait.go index 105f878786d..2a5ab08b43b 100644 --- a/cmd/compose/wait.go +++ b/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.Service) *cobra.Command { +func waitCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { opts := waitOptions{ ProjectOptions: p, } @@ -47,7 +48,7 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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,17 @@ func waitCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) return cmd } -func runWait(ctx context.Context, dockerCli command.Cli, backend api.Service, opts *waitOptions) (int64, error) { +func runWait(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, opts *waitOptions) (int64, error) { _, name, err := opts.projectOrName(ctx, dockerCli) if err != nil { return 0, err } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return 0, err + } + return backend.Wait(ctx, name, api.WaitOptions{ Services: opts.services, DownProjectOnContainerExit: opts.downProject, diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go index 9fe2293f426..f71ab765dfb 100644 --- a/cmd/compose/watch.go +++ b/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.Service) *cobra.Command { +func watchCommand(p *ProjectOptions, dockerCli command.Cli, backendOptions []compose.Option) *cobra.Command { watchOpts := watchOptions{ ProjectOptions: p, } @@ -53,7 +54,7 @@ func watchCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) 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.Service) return cmd } -func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, watchOpts watchOptions, buildOpts buildOptions, services []string) error { +func runWatch(ctx context.Context, dockerCli command.Cli, backendOptions []compose.Option, watchOpts watchOptions, buildOpts buildOptions, services []string) error { project, _, err := watchOpts.ToProject(ctx, dockerCli, services) if err != nil { return err @@ -88,6 +89,11 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w return fmt.Errorf("cannot take exclusive lock for project %q: %w", project.Name, err) } + backend, err := compose.NewComposeService(dockerCli, backendOptions...) + if err != nil { + return err + } + if !watchOpts.noUp { for index, service := range project.Services { if service.Build != nil && service.Develop != nil { diff --git a/cmd/main.go b/cmd/main.go index 46f0e9aa72e..7537147fa52 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,21 +30,19 @@ import ( "github.com/docker/compose/v2/cmd/compatibility" commands "github.com/docker/compose/v2/cmd/compose" "github.com/docker/compose/v2/internal" - "github.com/docker/compose/v2/pkg/compose" ) func pluginMain() { plugin.Run( - func(dockerCli command.Cli) *cobra.Command { - backend := compose.NewComposeService(dockerCli) - cmd := commands.RootCommand(dockerCli, backend) + func(cli command.Cli) *cobra.Command { + cmd := commands.RootCommand(cli) originalPreRunE := cmd.PersistentPreRunE cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { // initialize the dockerCli instance if err := plugin.PersistentPreRunE(cmd, args); err != nil { return err } - if err := cmdtrace.Setup(cmd, dockerCli, os.Args[1:]); err != nil { + if err := cmdtrace.Setup(cmd, cli, os.Args[1:]); err != nil { logrus.Debugf("failed to enable tracing: %v", err) } diff --git a/pkg/prompt/prompt.go b/cmd/prompt/prompt.go similarity index 97% rename from pkg/prompt/prompt.go rename to cmd/prompt/prompt.go index 07dbbcb4279..88d05039b6e 100644 --- a/pkg/prompt/prompt.go +++ b/cmd/prompt/prompt.go @@ -32,6 +32,10 @@ type UI interface { Confirm(message string, defaultValue bool) (bool, error) } +func Yes(_ string, _ bool) (bool, error) { + return true, nil +} + func NewPrompt(stdin *streams.In, stdout *streams.Out) UI { if stdin.IsTerminal() { return User{stdin: streamsFileReader{stdin}, stdout: streamsFileWriter{stdout}} diff --git a/pkg/prompt/prompt_mock.go b/cmd/prompt/prompt_mock.go similarity index 100% rename from pkg/prompt/prompt_mock.go rename to cmd/prompt/prompt_mock.go diff --git a/docs/yaml/main/generate.go b/docs/yaml/main/generate.go index 0908616837b..b900f877887 100644 --- a/docs/yaml/main/generate.go +++ b/docs/yaml/main/generate.go @@ -36,7 +36,7 @@ func generateDocs(opts *options) error { Use: "docker", DisableAutoGenTag: true, } - cmd.AddCommand(compose.RootCommand(dockerCLI, nil)) + cmd.AddCommand(compose.RootCommand(dockerCLI)) disableFlagsInUseLine(cmd) tool, err := clidocstool.New(clidocstool.Options{ diff --git a/pkg/api/api.go b/pkg/api/api.go index c82c8618a4f..fc7bc2df912 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -82,10 +82,6 @@ type Service 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 @@ -231,8 +227,6 @@ type CreateOptions struct { Timeout *time.Duration // QuietPull makes the pulling process quiet QuietPull bool - // AssumeYes assume "yes" as answer to all prompts and run non-interactively - AssumeYes bool } // StartOptions group options of the Start API diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index f9c3707d39f..ba632aff848 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -50,18 +50,71 @@ func init() { } } +type Option func(*composeService) error + // NewComposeService create a local implementation of the compose.Service API -func NewComposeService(dockerCli command.Cli) api.Service { - return &composeService{ - dockerCli: dockerCli, +func NewComposeService(dockerCli command.Cli, opts ...Option) (api.Service, error) { + s := &composeService{ + dockerCli: dockerCli, + prompt: func(message string, defaultValue bool) (bool, error) { + return false, errors.New("compose service has been used without a prompt to interact with user") + }, clock: clockwork.NewRealClock(), maxConcurrency: -1, dryRun: false, } + for _, opt := range opts { + if err := opt(s); err != nil { + return nil, err + } + } + 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) 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 let Compose service run without actually applying change to live resources +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) + type composeService struct { - dockerCli command.Cli + dockerCli command.Cli + + // prompt is used to interact with user and confirm actions + prompt Prompt + clock clockwork.Clock maxConcurrency int dryRun bool @@ -87,31 +140,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() } diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 5ff84f395b4..79b10355660 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -44,7 +44,6 @@ import ( "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/progress" - "github.com/docker/compose/v2/pkg/prompt" ) type createOptions struct { @@ -94,7 +93,7 @@ func (s *composeService) create(ctx context.Context, project *types.Project, opt return err } - volumes, err := s.ensureProjectVolumes(ctx, project, options.AssumeYes) + volumes, err := s.ensureProjectVolumes(ctx, project) if err != nil { return err } @@ -151,13 +150,13 @@ func (s *composeService) ensureNetworks(ctx context.Context, project *types.Proj return networks, nil } -func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project, assumeYes bool) (map[string]string, error) { +func (s *composeService) ensureProjectVolumes(ctx context.Context, project *types.Project) (map[string]string, error) { ids := map[string]string{} for k, volume := range project.Volumes { volume.CustomLabels = volume.CustomLabels.Add(api.VolumeLabel, k) volume.CustomLabels = volume.CustomLabels.Add(api.ProjectLabel, project.Name) volume.CustomLabels = volume.CustomLabels.Add(api.VersionLabel, api.ComposeVersion) - id, err := s.ensureVolume(ctx, k, volume, project, assumeYes) + id, err := s.ensureVolume(ctx, k, volume, project) if err != nil { return nil, err } @@ -1531,7 +1530,7 @@ func (s *composeService) resolveExternalNetwork(ctx context.Context, n *types.Ne } } -func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project, assumeYes bool) (string, error) { +func (s *composeService) ensureVolume(ctx context.Context, name string, volume types.VolumeConfig, project *types.Project) (string, error) { inspected, err := s.apiClient().VolumeInspect(ctx, volume.Name) if err != nil { if !errdefs.IsNotFound(err) { @@ -1563,13 +1562,10 @@ func (s *composeService) ensureVolume(ctx context.Context, name string, volume t } actual, ok := inspected.Labels[api.ConfigHashLabel] if ok && actual != expected { - confirm := assumeYes - if !assumeYes { - msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file. Recreate (data will be lost)?", volume.Name) - confirm, err = prompt.NewPrompt(s.stdin(), s.stdout()).Confirm(msg, false) - if err != nil { - return "", err - } + msg := fmt.Sprintf("Volume %q exists but doesn't match configuration in compose file. Recreate (data will be lost)?", volume.Name) + confirm, err := s.prompt(msg, false) + if err != nil { + return "", err } if confirm { err = s.removeDivergedVolume(ctx, name, volume, project) diff --git a/pkg/compose/publish.go b/pkg/compose/publish.go index a504dc15115..f7e05c1a3cf 100644 --- a/pkg/compose/publish.go +++ b/pkg/compose/publish.go @@ -25,18 +25,17 @@ import ( "fmt" "io" "os" + "strings" "github.com/DefangLabs/secret-detector/pkg/scanner" "github.com/DefangLabs/secret-detector/pkg/secrets" "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" "github.com/distribution/reference" - "github.com/docker/cli/cli/command" "github.com/docker/compose/v2/internal/oci" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/compose/transform" "github.com/docker/compose/v2/pkg/progress" - "github.com/docker/compose/v2/pkg/prompt" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -280,16 +279,21 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp } bindMounts := s.checkForBindMount(project) if len(bindMounts) > 0 { - fmt.Println("you are about to publish bind mounts declaration within your OCI artifact.\n" + + b := strings.Builder{} + b.WriteString("you are about to publish bind mounts declaration within your OCI artifact.\n" + "only the bind mount declarations will be added to the OCI artifact (not content)\n" + - "please double check that you are not mounting potential user's sensitive directories or data") + "please double check that you are not mounting potential user's sensitive directories or data:\n") for key, val := range bindMounts { - _, _ = fmt.Fprintln(s.dockerCli.Out(), key) + b.WriteString(key) + b.WriteRune('\n') for _, v := range val { - _, _ = fmt.Fprintf(s.dockerCli.Out(), "%s\n", v.String()) + b.WriteString(v.String()) + b.WriteRune('\n') } } - if ok, err := acceptPublishBindMountDeclarations(s.dockerCli); err != nil || !ok { + b.WriteString("Are you ok to publish these bind mount declarations? ") + ok, err := s.prompt(b.String(), false) + if err != nil || !ok { return false, err } } @@ -298,38 +302,29 @@ func (s *composeService) preChecks(project *types.Project, options api.PublishOp return false, err } if len(detectedSecrets) > 0 { - fmt.Println("you are about to publish sensitive data within your OCI artifact.\n" + - "please double check that you are not leaking sensitive data") + b := strings.Builder{} + b.WriteString("you are about to publish sensitive data within your OCI artifact.\n" + + "please double check that you are not leaking sensitive data\n") for _, val := range detectedSecrets { - _, _ = fmt.Fprintln(s.dockerCli.Out(), val.Type) - _, _ = fmt.Fprintf(s.dockerCli.Out(), "%q: %s\n", val.Key, val.Value) + b.WriteString(fmt.Sprintf("%s %q: %s\n", val.Type, val.Key, val.Value)) } - if ok, err := acceptPublishSensitiveData(s.dockerCli); err != nil || !ok { + b.WriteString("Are you ok to publish these sensitive data? ") + ok, err := s.prompt(b.String(), false) + if err != nil || !ok { return false, err } } - envVariables, err := s.checkEnvironmentVariables(project, options) - if err != nil { - return false, err - } - if len(envVariables) > 0 { - fmt.Println("you are about to publish environment variables within your OCI artifact.\n" + - "please double check that you are not leaking sensitive data") - for key, val := range envVariables { - _, _ = fmt.Fprintln(s.dockerCli.Out(), "Service/Config ", key) - for k, v := range val { - _, _ = fmt.Fprintf(s.dockerCli.Out(), "%s=%v\n", k, *v) - } - } - if ok, err := acceptPublishEnvVariables(s.dockerCli); err != nil || !ok { + if !options.WithEnvironment { + err := s.checkEnvironmentVariables(project) + if err != nil { return false, err } } + return true, nil } -func (s *composeService) checkEnvironmentVariables(project *types.Project, options api.PublishOptions) (map[string]types.MappingWithEquals, error) { - envVarList := map[string]types.MappingWithEquals{} +func (s *composeService) checkEnvironmentVariables(project *types.Project) error { errorList := map[string][]string{} for _, service := range project.Services { @@ -338,18 +333,16 @@ func (s *composeService) checkEnvironmentVariables(project *types.Project, optio } if len(service.Environment) > 0 { errorList[service.Name] = append(errorList[service.Name], fmt.Sprintf("service %q has environment variable(s) declared.", service.Name)) - envVarList[service.Name] = service.Environment } } for _, config := range project.Configs { if config.Environment != "" { errorList[config.Name] = append(errorList[config.Name], fmt.Sprintf("config %q is declare as an environment variable.", config.Name)) - envVarList[config.Name] = types.NewMappingWithEquals([]string{fmt.Sprintf("%s=%s", config.Name, config.Environment)}) } } - if !options.WithEnvironment && len(errorList) > 0 { + if len(errorList) > 0 { errorMsgSuffix := "To avoid leaking sensitive data, you must either explicitly allow the sending of environment variables by using the --with-env flag,\n" + "or remove sensitive data from your Compose configuration" errorMsg := "" @@ -358,28 +351,9 @@ func (s *composeService) checkEnvironmentVariables(project *types.Project, optio errorMsg += fmt.Sprintf("%s\n", err) } } - return nil, fmt.Errorf("%s%s", errorMsg, errorMsgSuffix) - + return fmt.Errorf("%s%s", errorMsg, errorMsgSuffix) } - return envVarList, nil -} - -func acceptPublishEnvVariables(cli command.Cli) (bool, error) { - msg := "Are you ok to publish these environment variables? [y/N]: " - confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false) - return confirm, err -} - -func acceptPublishSensitiveData(cli command.Cli) (bool, error) { - msg := "Are you ok to publish these sensitive data? [y/N]: " - confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false) - return confirm, err -} - -func acceptPublishBindMountDeclarations(cli command.Cli) (bool, error) { - msg := "Are you ok to publish these bind mount declarations? [y/N]: " - confirm, err := prompt.NewPrompt(cli.In(), cli.Out()).Confirm(msg, false) - return confirm, err + return nil } func envFileLayers(project *types.Project) []v1.Descriptor { diff --git a/pkg/compose/remove.go b/pkg/compose/remove.go index e8515a79a77..64ce5c4cab0 100644 --- a/pkg/compose/remove.go +++ b/pkg/compose/remove.go @@ -26,7 +26,6 @@ import ( "golang.org/x/sync/errgroup" "github.com/docker/compose/v2/pkg/progress" - "github.com/docker/compose/v2/pkg/prompt" ) func (s *composeService) Remove(ctx context.Context, projectName string, options api.RemoveOptions) error { @@ -85,7 +84,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options if options.Force { _, _ = fmt.Fprintln(s.stdout(), msg) } else { - confirm, err := prompt.NewPrompt(s.stdin(), s.stdout()).Confirm(msg, false) + confirm, err := s.prompt(msg, false) if err != nil { return err }