Browse Source

don't assume os.Stdout and rely on dockerCLI.streams

Signed-off-by: Nicolas De Loof <[email protected]>
Nicolas De Loof 2 years ago
parent
commit
24f83271f2

+ 2 - 2
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

+ 14 - 15
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),

+ 20 - 20
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

+ 5 - 5
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
 		},

+ 2 - 3
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.")

+ 5 - 6
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)

+ 5 - 6
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)
 		}

+ 4 - 5
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,

+ 4 - 4
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
 }

+ 6 - 7
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")
 }

+ 21 - 6
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
+}

+ 2 - 3
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")

+ 5 - 6
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 {

+ 4 - 5
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

+ 5 - 6
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
 }

+ 1 - 1
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

+ 29 - 0
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
+}

+ 1 - 1
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)

+ 0 - 1
pkg/compose/printer.go

@@ -94,7 +94,6 @@ func (p *printer) Run(cascadeStop bool, exitCodeFrom string, stopFn func() error
 				if cascadeStop {
 					if !aborting {
 						aborting = true
-						fmt.Println("Aborting on container exit...")
 						err := stopFn()
 						if err != nil {
 							return 0, err

+ 1 - 1
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 {

+ 2 - 1
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
 	}()