diff --git a/cmd/compose/build.go b/cmd/compose/build.go index 51602fddf3..28650e52cc 100644 --- a/cmd/compose/build.go +++ b/cmd/compose/build.go @@ -73,7 +73,7 @@ var printerModes = []string{ buildx.PrinterModeQuiet, } -func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func buildCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := buildOptions{ ProjectOptions: p, } @@ -82,7 +82,7 @@ func buildCommand(p *ProjectOptions, backend api.Service) *cobra.Command { Short: "Build or rebuild services", PreRunE: Adapt(func(ctx context.Context, args []string) error { if opts.memory != "" { - fmt.Println("WARNING --memory is ignored as not supported in buildkit.") + fmt.Fprintln(streams.Err(), "WARNING --memory is ignored as not supported in buildkit.") } if opts.quiet { opts.progress = buildx.PrinterModeQuiet diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 7c61ffe203..04ada280bb 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -31,7 +31,6 @@ import ( "github.com/docker/buildx/util/logutil" dockercli "github.com/docker/cli/cli" "github.com/docker/cli/cli-plugins/manager" - "github.com/docker/cli/cli/command" "github.com/morikuni/aec" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -243,7 +242,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(streams api.Streams, backend api.Service) *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 @@ -305,7 +304,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // if verbose { logrus.SetLevel(logrus.TraceLevel) } - formatter.SetANSIMode(ansi) + formatter.SetANSIMode(streams, ansi) switch ansi { case "never": progress.Mode = progress.ModePlain @@ -333,27 +332,27 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // } c.AddCommand( - upCommand(&opts, backend), + upCommand(&opts, streams, backend), downCommand(&opts, backend), startCommand(&opts, backend), restartCommand(&opts, backend), stopCommand(&opts, backend), - psCommand(&opts, backend), - listCommand(backend), - logsCommand(&opts, backend), - convertCommand(&opts, backend), + psCommand(&opts, streams, backend), + listCommand(streams, backend), + logsCommand(&opts, streams, backend), + convertCommand(&opts, streams, backend), killCommand(&opts, backend), - runCommand(&opts, dockerCli, backend), + runCommand(&opts, streams, backend), removeCommand(&opts, backend), - execCommand(&opts, dockerCli, backend), + execCommand(&opts, streams, backend), pauseCommand(&opts, backend), unpauseCommand(&opts, backend), - topCommand(&opts, backend), - eventsCommand(&opts, backend), - portCommand(&opts, backend), - imagesCommand(&opts, backend), + topCommand(&opts, streams, backend), + eventsCommand(&opts, streams, backend), + portCommand(&opts, streams, backend), + imagesCommand(&opts, streams, backend), versionCommand(), - buildCommand(&opts, backend), + buildCommand(&opts, streams, backend), pushCommand(&opts, backend), pullCommand(&opts, backend), createCommand(&opts, backend), diff --git a/cmd/compose/convert.go b/cmd/compose/convert.go index 080451b97b..e1d5c5930d 100644 --- a/cmd/compose/convert.go +++ b/cmd/compose/convert.go @@ -50,7 +50,7 @@ type convertOptions struct { noConsistency bool } -func convertCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func convertCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := convertOptions{ ProjectOptions: p, } @@ -73,22 +73,22 @@ func convertCommand(p *ProjectOptions, backend api.Service) *cobra.Command { }), RunE: Adapt(func(ctx context.Context, args []string) error { if opts.services { - return runServices(opts) + return runServices(streams, opts) } if opts.volumes { - return runVolumes(opts) + return runVolumes(streams, opts) } if opts.hash != "" { - return runHash(opts) + return runHash(streams, opts) } if opts.profiles { - return runProfiles(opts, args) + return runProfiles(streams, opts, args) } if opts.images { - return runConfigImages(opts, args) + return runConfigImages(streams, opts, args) } - return runConvert(ctx, backend, opts, args) + return runConvert(ctx, streams, backend, opts, args) }), ValidArgsFunction: completeServiceNames(p), } @@ -110,7 +110,7 @@ func convertCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return cmd } -func runConvert(ctx context.Context, backend api.Service, opts convertOptions, services []string) error { +func runConvert(ctx context.Context, streams api.Streams, backend api.Service, opts convertOptions, services []string) error { var content []byte project, err := opts.ToProject(services, cli.WithInterpolation(!opts.noInterpolate), @@ -139,7 +139,7 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s return nil } - var out io.Writer = os.Stdout + var out io.Writer = streams.Out() if opts.Output != "" && len(content) > 0 { file, err := os.Create(opts.Output) if err != nil { @@ -151,29 +151,29 @@ func runConvert(ctx context.Context, backend api.Service, opts convertOptions, s return err } -func runServices(opts convertOptions) error { +func runServices(streams api.Streams, opts convertOptions) error { project, err := opts.ToProject(nil) if err != nil { return err } return project.WithServices(project.ServiceNames(), func(s types.ServiceConfig) error { - fmt.Println(s.Name) + fmt.Fprintln(streams.Out(), s.Name) return nil }) } -func runVolumes(opts convertOptions) error { +func runVolumes(streams api.Streams, opts convertOptions) error { project, err := opts.ToProject(nil) if err != nil { return err } for n := range project.Volumes { - fmt.Println(n) + fmt.Fprintln(streams.Out(), n) } return nil } -func runHash(opts convertOptions) error { +func runHash(streams api.Streams, opts convertOptions) error { var services []string if opts.hash != "*" { services = append(services, strings.Split(opts.hash, ",")...) @@ -187,12 +187,12 @@ func runHash(opts convertOptions) error { if err != nil { return err } - fmt.Printf("%s %s\n", s.Name, hash) + fmt.Fprintf(streams.Out(), "%s %s\n", s.Name, hash) } return nil } -func runProfiles(opts convertOptions, services []string) error { +func runProfiles(streams api.Streams, opts convertOptions, services []string) error { set := map[string]struct{}{} project, err := opts.ToProject(services) if err != nil { @@ -209,21 +209,21 @@ func runProfiles(opts convertOptions, services []string) error { } sort.Strings(profiles) for _, p := range profiles { - fmt.Println(p) + fmt.Fprintln(streams.Out(), p) } return nil } -func runConfigImages(opts convertOptions, services []string) error { +func runConfigImages(streams api.Streams, opts convertOptions, services []string) error { project, err := opts.ToProject(services) if err != nil { return err } for _, s := range project.Services { if s.Image != "" { - fmt.Println(s.Image) + fmt.Fprintln(streams.Out(), s.Image) } else { - fmt.Printf("%s%s%s\n", project.Name, api.Separator, s.Name) + fmt.Fprintf(streams.Out(), "%s%s%s\n", project.Name, api.Separator, s.Name) } } return nil diff --git a/cmd/compose/events.go b/cmd/compose/events.go index bea8138f51..738d8265d3 100644 --- a/cmd/compose/events.go +++ b/cmd/compose/events.go @@ -31,7 +31,7 @@ type eventsOpts struct { json bool } -func eventsCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func eventsCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := eventsOpts{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -41,7 +41,7 @@ func eventsCommand(p *ProjectOptions, backend api.Service) *cobra.Command { Use: "events [OPTIONS] [SERVICE...]", Short: "Receive real time events from containers.", RunE: Adapt(func(ctx context.Context, args []string) error { - return runEvents(ctx, backend, opts, args) + return runEvents(ctx, streams, backend, opts, args) }), ValidArgsFunction: completeServiceNames(p), } @@ -50,7 +50,7 @@ func eventsCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return cmd } -func runEvents(ctx context.Context, backend api.Service, opts eventsOpts, services []string) error { +func runEvents(ctx context.Context, streams api.Streams, backend api.Service, opts eventsOpts, services []string) error { name, err := opts.toProjectName() if err != nil { return err @@ -71,9 +71,9 @@ func runEvents(ctx context.Context, backend api.Service, opts eventsOpts, servic if err != nil { return err } - fmt.Println(string(marshal)) + fmt.Fprintln(streams.Out(), string(marshal)) } else { - fmt.Println(event) + fmt.Fprintln(streams.Out(), event) } return nil }, diff --git a/cmd/compose/exec.go b/cmd/compose/exec.go index 8458e6389e..4c24841f9b 100644 --- a/cmd/compose/exec.go +++ b/cmd/compose/exec.go @@ -21,7 +21,6 @@ import ( "github.com/compose-spec/compose-go/types" "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" @@ -43,7 +42,7 @@ type execOpts struct { interactive bool } -func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func execCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := execOpts{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -69,7 +68,7 @@ func execCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) runCmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if there are multiple instances of a service [default: 1].") runCmd.Flags().BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the process.") runCmd.Flags().StringVarP(&opts.user, "user", "u", "", "Run the command as this user.") - runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.") + runCmd.Flags().BoolVarP(&opts.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation. By default `docker compose exec` allocates a TTY.") runCmd.Flags().StringVarP(&opts.workingDir, "workdir", "w", "", "Path to workdir directory for this command.") runCmd.Flags().BoolVarP(&opts.interactive, "interactive", "i", true, "Keep STDIN open even if not attached.") diff --git a/cmd/compose/images.go b/cmd/compose/images.go index 249e05aeca..7bb9ea3678 100644 --- a/cmd/compose/images.go +++ b/cmd/compose/images.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io" - "os" "sort" "strings" @@ -39,7 +38,7 @@ type imageOptions struct { Format string } -func imagesCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func imagesCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := imageOptions{ ProjectOptions: p, } @@ -47,7 +46,7 @@ func imagesCommand(p *ProjectOptions, backend api.Service) *cobra.Command { Use: "images [OPTIONS] [SERVICE...]", Short: "List images used by the created containers", RunE: Adapt(func(ctx context.Context, args []string) error { - return runImages(ctx, backend, opts, args) + return runImages(ctx, streams, backend, opts, args) }), ValidArgsFunction: completeServiceNames(p), } @@ -56,7 +55,7 @@ func imagesCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return imgCmd } -func runImages(ctx context.Context, backend api.Service, opts imageOptions, services []string) error { +func runImages(ctx context.Context, streams api.Streams, backend api.Service, opts imageOptions, services []string) error { projectName, err := opts.toProjectName() if err != nil { return err @@ -81,7 +80,7 @@ func runImages(ctx context.Context, backend api.Service, opts imageOptions, serv } } for _, img := range ids { - fmt.Println(img) + fmt.Fprintln(streams.Out(), img) } return nil } @@ -90,7 +89,7 @@ func runImages(ctx context.Context, backend api.Service, opts imageOptions, serv return images[i].ContainerName < images[j].ContainerName }) - return formatter.Print(images, opts.Format, os.Stdout, + return formatter.Print(images, opts.Format, streams.Out(), func(w io.Writer) { for _, img := range images { id := stringid.TruncateID(img.ID) diff --git a/cmd/compose/list.go b/cmd/compose/list.go index 890757a581..4eac06da8a 100644 --- a/cmd/compose/list.go +++ b/cmd/compose/list.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io" - "os" "strings" "github.com/docker/compose/v2/cmd/formatter" @@ -38,13 +37,13 @@ type lsOptions struct { Filter opts.FilterOpt } -func listCommand(backend api.Service) *cobra.Command { +func listCommand(streams api.Streams, backend api.Service) *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, backend, lsOpts) + return runList(ctx, streams, backend, lsOpts) }), Args: cobra.NoArgs, ValidArgsFunction: noCompletion(), @@ -61,7 +60,7 @@ var acceptedListFilters = map[string]bool{ "name": true, } -func runList(ctx context.Context, backend api.Service, lsOpts lsOptions) error { +func runList(ctx context.Context, streams api.Streams, backend api.Service, lsOpts lsOptions) error { filters := lsOpts.Filter.Value() err := filters.Validate(acceptedListFilters) if err != nil { @@ -74,7 +73,7 @@ func runList(ctx context.Context, backend api.Service, lsOpts lsOptions) error { } if lsOpts.Quiet { for _, s := range stackList { - fmt.Println(s.Name) + fmt.Fprintln(streams.Out(), s.Name) } return nil } @@ -91,7 +90,7 @@ func runList(ctx context.Context, backend api.Service, lsOpts lsOptions) error { } view := viewFromStackList(stackList) - return formatter.Print(view, lsOpts.Format, os.Stdout, func(w io.Writer) { + return formatter.Print(view, lsOpts.Format, streams.Out(), func(w io.Writer) { for _, stack := range view { _, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", stack.Name, stack.Status, stack.ConfigFiles) } diff --git a/cmd/compose/logs.go b/cmd/compose/logs.go index 3a0966c92c..3e0123e2ac 100644 --- a/cmd/compose/logs.go +++ b/cmd/compose/logs.go @@ -18,7 +18,6 @@ package compose import ( "context" - "os" "github.com/spf13/cobra" @@ -38,7 +37,7 @@ type logsOptions struct { timestamps bool } -func logsCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func logsCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := logsOptions{ ProjectOptions: p, } @@ -46,7 +45,7 @@ func logsCommand(p *ProjectOptions, backend api.Service) *cobra.Command { Use: "logs [OPTIONS] [SERVICE...]", Short: "View output from containers", RunE: Adapt(func(ctx context.Context, args []string) error { - return runLogs(ctx, backend, opts, args) + return runLogs(ctx, streams, backend, opts, args) }), ValidArgsFunction: completeServiceNames(p), } @@ -61,12 +60,12 @@ func logsCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return logsCmd } -func runLogs(ctx context.Context, backend api.Service, opts logsOptions, services []string) error { +func runLogs(ctx context.Context, streams api.Streams, backend api.Service, opts logsOptions, services []string) error { project, name, err := opts.projectOrName(services...) if err != nil { return err } - consumer := formatter.NewLogConsumer(ctx, os.Stdout, os.Stderr, !opts.noColor, !opts.noPrefix, false) + consumer := formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !opts.noColor, !opts.noPrefix, false) return backend.Logs(ctx, name, consumer, api.LogOptions{ Project: project, Services: services, diff --git a/cmd/compose/port.go b/cmd/compose/port.go index 2d1f1b28d3..7d9aacb807 100644 --- a/cmd/compose/port.go +++ b/cmd/compose/port.go @@ -34,7 +34,7 @@ type portOptions struct { index int } -func portCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func portCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := portOptions{ ProjectOptions: p, } @@ -52,7 +52,7 @@ func portCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return nil }), RunE: Adapt(func(ctx context.Context, args []string) error { - return runPort(ctx, backend, opts, args[0]) + return runPort(ctx, streams, backend, opts, args[0]) }), ValidArgsFunction: completeServiceNames(p), } @@ -61,7 +61,7 @@ func portCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return cmd } -func runPort(ctx context.Context, backend api.Service, opts portOptions, service string) error { +func runPort(ctx context.Context, streams api.Streams, backend api.Service, opts portOptions, service string) error { projectName, err := opts.toProjectName() if err != nil { return err @@ -74,6 +74,6 @@ func runPort(ctx context.Context, backend api.Service, opts portOptions, service return err } - fmt.Printf("%s:%d\n", ip, port) + fmt.Fprintf(streams.Out(), "%s:%d\n", ip, port) return nil } diff --git a/cmd/compose/ps.go b/cmd/compose/ps.go index c0d34facf7..0cf9b91656 100644 --- a/cmd/compose/ps.go +++ b/cmd/compose/ps.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io" - "os" "sort" "strconv" "strings" @@ -67,7 +66,7 @@ func (p *psOptions) parseFilter() error { return nil } -func psCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func psCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := psOptions{ ProjectOptions: p, } @@ -78,7 +77,7 @@ func psCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return opts.parseFilter() }, RunE: Adapt(func(ctx context.Context, args []string) error { - return runPs(ctx, backend, args, opts) + return runPs(ctx, streams, backend, args, opts) }), ValidArgsFunction: completeServiceNames(p), } @@ -92,7 +91,7 @@ func psCommand(p *ProjectOptions, backend api.Service) *cobra.Command { return psCmd } -func runPs(ctx context.Context, backend api.Service, services []string, opts psOptions) error { +func runPs(ctx context.Context, streams api.Streams, backend api.Service, services []string, opts psOptions) error { project, name, err := opts.projectOrName(services...) if err != nil { return err @@ -126,7 +125,7 @@ SERVICES: if opts.Quiet { for _, c := range containers { - fmt.Println(c.ID) + fmt.Fprintln(streams.Out(), c.ID) } return nil } @@ -138,11 +137,11 @@ SERVICES: services = append(services, s.Service) } } - fmt.Println(strings.Join(services, "\n")) + fmt.Fprintln(streams.Out(), strings.Join(services, "\n")) return nil } - return formatter.Print(containers, opts.Format, os.Stdout, + return formatter.Print(containers, opts.Format, streams.Out(), writer(containers), "NAME", "IMAGE", "COMMAND", "SERVICE", "CREATED", "STATUS", "PORTS") } diff --git a/cmd/compose/ps_test.go b/cmd/compose/ps_test.go index a6fa71f88a..621214ff3c 100644 --- a/cmd/compose/ps_test.go +++ b/cmd/compose/ps_test.go @@ -18,10 +18,12 @@ package compose import ( "context" + "io" "os" "path/filepath" "testing" + "github.com/docker/cli/cli/streams" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/mocks" "github.com/golang/mock/gomock" @@ -30,10 +32,6 @@ import ( func TestPsTable(t *testing.T) { ctx := context.Background() - origStdout := os.Stdout - t.Cleanup(func() { - os.Stdout = origStdout - }) dir := t.TempDir() out := filepath.Join(dir, "output.txt") f, err := os.Create(out) @@ -42,7 +40,6 @@ func TestPsTable(t *testing.T) { } defer func() { _ = f.Close() }() - os.Stdout = f ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -72,7 +69,7 @@ func TestPsTable(t *testing.T) { }).AnyTimes() opts := psOptions{ProjectOptions: &ProjectOptions{ProjectName: "test"}} - err = runPs(ctx, backend, nil, opts) + err = runPs(ctx, stream{out: streams.NewOut(f)}, backend, nil, opts) assert.NoError(t, err) _, err = f.Seek(0, 0) @@ -83,3 +80,21 @@ func TestPsTable(t *testing.T) { assert.Contains(t, string(output), "8080/tcp, 8443/tcp") } + +type stream struct { + out *streams.Out + err io.Writer + in *streams.In +} + +func (s stream) Out() *streams.Out { + return s.out +} + +func (s stream) Err() io.Writer { + return s.err +} + +func (s stream) In() *streams.In { + return s.in +} diff --git a/cmd/compose/run.go b/cmd/compose/run.go index 9939e3eec8..344ee50c72 100644 --- a/cmd/compose/run.go +++ b/cmd/compose/run.go @@ -24,7 +24,6 @@ import ( cgo "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" - "github.com/docker/cli/cli/command" "github.com/mattn/go-shellwords" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -108,7 +107,7 @@ func (opts runOptions) apply(project *types.Project) error { return nil } -func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { +func runCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := runOptions{ composeOptions: &composeOptions{ ProjectOptions: p, @@ -151,7 +150,7 @@ func runCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) * flags.StringArrayVarP(&opts.environment, "env", "e", []string{}, "Set environment variables") flags.StringArrayVarP(&opts.labels, "label", "l", []string{}, "Add or override a label") flags.BoolVar(&opts.Remove, "rm", false, "Automatically remove the container when it exits") - flags.BoolVarP(&opts.noTty, "no-TTY", "T", !dockerCli.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).") + flags.BoolVarP(&opts.noTty, "no-TTY", "T", !streams.Out().IsTerminal(), "Disable pseudo-TTY allocation (default: auto-detected).") flags.StringVar(&opts.name, "name", "", "Assign a name to the container") flags.StringVarP(&opts.user, "user", "u", "", "Run as specified username or uid") flags.StringVarP(&opts.workdir, "workdir", "w", "", "Working directory inside the container") diff --git a/cmd/compose/top.go b/cmd/compose/top.go index 2ff34ca299..45d095bfbb 100644 --- a/cmd/compose/top.go +++ b/cmd/compose/top.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io" - "os" "sort" "strings" "text/tabwriter" @@ -34,7 +33,7 @@ type topOptions struct { *ProjectOptions } -func topCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func topCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { opts := topOptions{ ProjectOptions: p, } @@ -42,14 +41,14 @@ func topCommand(p *ProjectOptions, backend api.Service) *cobra.Command { Use: "top [SERVICES...]", Short: "Display the running processes", RunE: Adapt(func(ctx context.Context, args []string) error { - return runTop(ctx, backend, opts, args) + return runTop(ctx, streams, backend, opts, args) }), ValidArgsFunction: completeServiceNames(p), } return topCmd } -func runTop(ctx context.Context, backend api.Service, opts topOptions, services []string) error { +func runTop(ctx context.Context, streams api.Streams, backend api.Service, opts topOptions, services []string) error { projectName, err := opts.toProjectName() if err != nil { return err @@ -64,8 +63,8 @@ func runTop(ctx context.Context, backend api.Service, opts topOptions, services }) for _, container := range containers { - fmt.Printf("%s\n", container.Name) - err := psPrinter(os.Stdout, func(w io.Writer) { + fmt.Fprintf(streams.Out(), "%s\n", container.Name) + err := psPrinter(streams.Out(), func(w io.Writer) { for _, proc := range container.Processes { info := []interface{}{} for _, p := range proc { diff --git a/cmd/compose/up.go b/cmd/compose/up.go index 12b45d1c4d..6c2669d63f 100644 --- a/cmd/compose/up.go +++ b/cmd/compose/up.go @@ -19,7 +19,6 @@ package compose import ( "context" "fmt" - "os" "strconv" "strings" @@ -87,7 +86,7 @@ func (opts upOptions) apply(project *types.Project, services []string) error { return nil } -func upCommand(p *ProjectOptions, backend api.Service) *cobra.Command { +func upCommand(p *ProjectOptions, streams api.Streams, backend api.Service) *cobra.Command { up := upOptions{} create := createOptions{} upCmd := &cobra.Command{ @@ -102,7 +101,7 @@ func upCommand(p *ProjectOptions, backend api.Service) *cobra.Command { if create.ignoreOrphans && create.removeOrphans { return fmt.Errorf("COMPOSE_IGNORE_ORPHANS and --remove-orphans cannot be combined") } - return runUp(ctx, backend, create, up, project, services) + return runUp(ctx, streams, backend, create, up, project, services) }), ValidArgsFunction: completeServiceNames(p), } @@ -158,7 +157,7 @@ func validateFlags(up *upOptions, create *createOptions) error { return nil } -func runUp(ctx context.Context, backend api.Service, createOptions createOptions, upOptions upOptions, project *types.Project, services []string) error { +func runUp(ctx context.Context, streams api.Streams, backend api.Service, createOptions createOptions, upOptions upOptions, project *types.Project, services []string) error { if len(project.Services) == 0 { return fmt.Errorf("no service selected") } @@ -172,7 +171,7 @@ func runUp(ctx context.Context, backend api.Service, createOptions createOptions var consumer api.LogConsumer if !upOptions.Detach { - consumer = formatter.NewLogConsumer(ctx, os.Stdout, os.Stderr, !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp) + consumer = formatter.NewLogConsumer(ctx, streams.Out(), streams.Err(), !upOptions.noColor, !upOptions.noPrefix, upOptions.timestamp) } attachTo := services diff --git a/cmd/formatter/colors.go b/cmd/formatter/colors.go index 8c24808829..79afb5354d 100644 --- a/cmd/formatter/colors.go +++ b/cmd/formatter/colors.go @@ -18,10 +18,9 @@ package formatter import ( "fmt" - "os" "strconv" - "github.com/mattn/go-isatty" + "github.com/docker/compose/v2/pkg/api" ) var names = []string{ @@ -47,20 +46,20 @@ const ( ) // SetANSIMode configure formatter for colored output on ANSI-compliant console -func SetANSIMode(ansi string) { - if !useAnsi(ansi) { +func SetANSIMode(streams api.Streams, ansi string) { + if !useAnsi(streams, ansi) { nextColor = func() colorFunc { return monochrome } } } -func useAnsi(ansi string) bool { +func useAnsi(streams api.Streams, ansi string) bool { switch ansi { case Always: return true case Auto: - return isatty.IsTerminal(os.Stdout.Fd()) + return streams.Out().IsTerminal() } return false } diff --git a/go.mod b/go.mod index faf204267e..131b617141 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/golang/mock v1.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 - github.com/mattn/go-isatty v0.0.16 + github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-shellwords v1.0.12 github.com/moby/buildkit v0.10.4 // replaced; see replace rule for actual version github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f diff --git a/pkg/api/io.go b/pkg/api/io.go new file mode 100644 index 0000000000..3bb73e0ccb --- /dev/null +++ b/pkg/api/io.go @@ -0,0 +1,29 @@ +/* + 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 api + +import ( + "io" + + "github.com/docker/cli/cli/streams" +) + +type Streams interface { + Out() *streams.Out + Err() io.Writer + In() *streams.In +} diff --git a/pkg/compose/attach.go b/pkg/compose/attach.go index 29f961f8fa..c339f3f6bf 100644 --- a/pkg/compose/attach.go +++ b/pkg/compose/attach.go @@ -48,7 +48,7 @@ func (s *composeService) attach(ctx context.Context, project *types.Project, lis names = append(names, getContainerNameWithoutProject(c)) } - fmt.Printf("Attaching to %s\n", strings.Join(names, ", ")) + fmt.Fprintf(s.stdout(), "Attaching to %s\n", strings.Join(names, ", ")) for _, container := range containers { err := s.attachContainer(ctx, container, listener) diff --git a/pkg/compose/printer.go b/pkg/compose/printer.go index a46012767d..29932b8058 100644 --- a/pkg/compose/printer.go +++ b/pkg/compose/printer.go @@ -97,7 +97,6 @@ func (p *printer) Run(ctx context.Context, cascadeStop bool, exitCodeFrom string if cascadeStop { if !aborting { aborting = true - fmt.Println("Aborting on container exit...") err := stopFn() if err != nil { return 0, err diff --git a/pkg/compose/remove.go b/pkg/compose/remove.go index 557572b64d..2cb0b79864 100644 --- a/pkg/compose/remove.go +++ b/pkg/compose/remove.go @@ -59,7 +59,7 @@ func (s *composeService) Remove(ctx context.Context, projectName string, options } msg := fmt.Sprintf("Going to remove %s", strings.Join(names, ", ")) if options.Force { - fmt.Println(msg) + fmt.Fprintln(s.stdout(), msg) } else { confirm, err := prompt.User{}.Confirm(msg, false) if err != nil { diff --git a/pkg/compose/up.go b/pkg/compose/up.go index b8631e0eff..3c00a5a65f 100644 --- a/pkg/compose/up.go +++ b/pkg/compose/up.go @@ -55,6 +55,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) stopFunc := func() error { + fmt.Fprintln(s.stderr(), "Aborting on container exit...") ctx := context.Background() return progress.Run(ctx, func(ctx context.Context) error { go func() { @@ -74,7 +75,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options go func() { <-signalChan printer.Cancel() - fmt.Println("Gracefully stopping... (press Ctrl+C again to force)") + fmt.Fprintln(s.stderr(), "Gracefully stopping... (press Ctrl+C again to force)") stopFunc() //nolint:errcheck }()