cli: Wrap Err stream

This wraps the cli stderr stream the same way as stdin and stdout, which
extends the stream with TTY-related methods.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2024-06-11 16:49:25 +02:00
parent 52eddcf4e4
commit 6b93cf221a
No known key found for this signature in database
GPG Key ID: B85EFCFE26DEF92A
9 changed files with 25 additions and 24 deletions

View File

@ -44,7 +44,7 @@ const defaultInitTimeout = 2 * time.Second
type Streams interface { type Streams interface {
In() *streams.In In() *streams.In
Out() *streams.Out Out() *streams.Out
Err() io.Writer Err() *streams.Out
} }
// Cli represents the docker command line client. // Cli represents the docker command line client.
@ -75,7 +75,7 @@ type DockerCli struct {
options *cliflags.ClientOptions options *cliflags.ClientOptions
in *streams.In in *streams.In
out *streams.Out out *streams.Out
err io.Writer err *streams.Out
client client.APIClient client client.APIClient
serverInfo ServerInfo serverInfo ServerInfo
contentTrust bool contentTrust bool
@ -124,7 +124,7 @@ func (cli *DockerCli) Out() *streams.Out {
} }
// Err returns the writer used for stderr // Err returns the writer used for stderr
func (cli *DockerCli) Err() io.Writer { func (cli *DockerCli) Err() *streams.Out {
return cli.err return cli.err
} }

View File

@ -23,7 +23,7 @@ func WithStandardStreams() CLIOption {
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
cli.in = streams.NewIn(stdin) cli.in = streams.NewIn(stdin)
cli.out = streams.NewOut(stdout) cli.out = streams.NewOut(stdout)
cli.err = stderr cli.err = streams.NewOut(stderr)
return nil return nil
} }
} }
@ -40,8 +40,9 @@ func WithBaseContext(ctx context.Context) CLIOption {
// WithCombinedStreams uses the same stream for the output and error streams. // WithCombinedStreams uses the same stream for the output and error streams.
func WithCombinedStreams(combined io.Writer) CLIOption { func WithCombinedStreams(combined io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.out = streams.NewOut(combined) s := streams.NewOut(combined)
cli.err = combined cli.out = s
cli.err = s
return nil return nil
} }
} }
@ -65,7 +66,7 @@ func WithOutputStream(out io.Writer) CLIOption {
// WithErrorStream sets a cli error stream. // WithErrorStream sets a cli error stream.
func WithErrorStream(err io.Writer) CLIOption { func WithErrorStream(err io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.err = err cli.err = streams.NewOut(err)
return nil return nil
} }
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/flags"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -253,7 +254,7 @@ func TestExperimentalCLI(t *testing.T) {
}, },
} }
cli := &DockerCli{client: apiclient, err: os.Stderr} cli := &DockerCli{client: apiclient, err: streams.NewOut(os.Stderr)}
config.SetDir(dir.Path()) config.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions()) err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err) assert.NilError(t, err)

View File

@ -130,9 +130,9 @@ func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *
out := dockerCli.Err() out := dockerCli.Err()
if options.quiet { if options.quiet {
out = io.Discard out = streams.NewOut(io.Discard)
} }
return jsonmessage.DisplayJSONMessagesToStream(responseBody, streams.NewOut(out), nil) return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
} }
type cidFile struct { type cidFile struct {

View File

@ -18,7 +18,6 @@ import (
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/registry" "github.com/docker/docker/registry"
"github.com/moby/term"
"github.com/morikuni/aec" "github.com/morikuni/aec"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -176,7 +175,7 @@ func handleAux(dockerCli command.Cli) func(jm jsonmessage.JSONMessage) {
} }
func printNote(dockerCli command.Cli, format string, args ...any) { func printNote(dockerCli command.Cli, format string, args ...any) {
if _, isTTY := term.GetFdInfo(dockerCli.Err()); isTTY { if dockerCli.Err().IsTerminal() {
_, _ = fmt.Fprint(dockerCli.Err(), aec.WhiteF.Apply(aec.CyanB.Apply("[ NOTE ]"))+" ") _, _ = fmt.Fprint(dockerCli.Err(), aec.WhiteF.Apply(aec.CyanB.Apply("[ NOTE ]"))+" ")
} else { } else {
_, _ = fmt.Fprint(dockerCli.Err(), "[ NOTE ] ") _, _ = fmt.Fprint(dockerCli.Err(), "[ NOTE ] ")

View File

@ -8,6 +8,7 @@ import (
"testing" "testing"
configtypes "github.com/docker/cli/cli/config/types" configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/system" "github.com/docker/docker/api/types/system"
@ -69,7 +70,7 @@ func TestLoginWithCredStoreCreds(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
errBuf := new(bytes.Buffer) errBuf := new(bytes.Buffer)
cli.SetErr(errBuf) cli.SetErr(streams.NewOut(errBuf))
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig) loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
outputString := cli.OutBuffer().String() outputString := cli.OutBuffer().String()
assert.Check(t, is.Equal(tc.expectedMsg, outputString)) assert.Check(t, is.Equal(tc.expectedMsg, outputString))

View File

@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/docker/cli/cli/version" "github.com/docker/cli/cli/version"
"github.com/moby/term"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
@ -101,12 +100,10 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
} }
func stdioAttributes(streams Streams) []attribute.KeyValue { func stdioAttributes(streams Streams) []attribute.KeyValue {
// we don't wrap stderr, but we do wrap in/out
_, stderrTty := term.GetFdInfo(streams.Err())
return []attribute.KeyValue{ return []attribute.KeyValue{
attribute.Bool("command.stdin.isatty", streams.In().IsTerminal()), attribute.Bool("command.stdin.isatty", streams.In().IsTerminal()),
attribute.Bool("command.stdout.isatty", streams.Out().IsTerminal()), attribute.Bool("command.stdout.isatty", streams.Out().IsTerminal()),
attribute.Bool("command.stderr.isatty", stderrTty), attribute.Bool("command.stderr.isatty", streams.Err().IsTerminal()),
} }
} }

View File

@ -136,7 +136,7 @@ func TestStdioAttributes(t *testing.T) {
cli := &DockerCli{ cli := &DockerCli{
in: streams.NewIn(io.NopCloser(strings.NewReader(""))), in: streams.NewIn(io.NopCloser(strings.NewReader(""))),
out: streams.NewOut(outBuffer), out: streams.NewOut(outBuffer),
err: errBuffer, err: streams.NewOut(errBuffer),
} }
cli.In().SetIsTerminal(tc.stdinTty) cli.In().SetIsTerminal(tc.stdinTty)
cli.Out().SetIsTerminal(tc.stdoutTty) cli.Out().SetIsTerminal(tc.stdoutTty)

View File

@ -28,7 +28,8 @@ type FakeCli struct {
configfile *configfile.ConfigFile configfile *configfile.ConfigFile
out *streams.Out out *streams.Out
outBuffer *bytes.Buffer outBuffer *bytes.Buffer
err *bytes.Buffer err *streams.Out
errBuffer *bytes.Buffer
in *streams.In in *streams.In
server command.ServerInfo server command.ServerInfo
notaryClientFunc NotaryClientFuncType notaryClientFunc NotaryClientFuncType
@ -48,7 +49,8 @@ func NewFakeCli(apiClient client.APIClient, opts ...func(*FakeCli)) *FakeCli {
client: apiClient, client: apiClient,
out: streams.NewOut(outBuffer), out: streams.NewOut(outBuffer),
outBuffer: outBuffer, outBuffer: outBuffer,
err: errBuffer, err: streams.NewOut(errBuffer),
errBuffer: errBuffer,
in: streams.NewIn(io.NopCloser(strings.NewReader(""))), in: streams.NewIn(io.NopCloser(strings.NewReader(""))),
// Use an empty string for filename so that tests don't create configfiles // Use an empty string for filename so that tests don't create configfiles
// Set cli.ConfigFile().Filename to a tempfile to support Save. // Set cli.ConfigFile().Filename to a tempfile to support Save.
@ -67,7 +69,7 @@ func (c *FakeCli) SetIn(in *streams.In) {
} }
// SetErr sets the stderr stream for the cli to the specified io.Writer // SetErr sets the stderr stream for the cli to the specified io.Writer
func (c *FakeCli) SetErr(err *bytes.Buffer) { func (c *FakeCli) SetErr(err *streams.Out) {
c.err = err c.err = err
} }
@ -112,7 +114,7 @@ func (c *FakeCli) Out() *streams.Out {
} }
// Err returns the output stream (stderr) the cli should write on // Err returns the output stream (stderr) the cli should write on
func (c *FakeCli) Err() io.Writer { func (c *FakeCli) Err() *streams.Out {
return c.err return c.err
} }
@ -153,13 +155,13 @@ func (c *FakeCli) OutBuffer() *bytes.Buffer {
// ErrBuffer Buffer returns the stderr buffer // ErrBuffer Buffer returns the stderr buffer
func (c *FakeCli) ErrBuffer() *bytes.Buffer { func (c *FakeCli) ErrBuffer() *bytes.Buffer {
return c.err return c.errBuffer
} }
// ResetOutputBuffers resets the .OutBuffer() and.ErrBuffer() back to empty // ResetOutputBuffers resets the .OutBuffer() and.ErrBuffer() back to empty
func (c *FakeCli) ResetOutputBuffers() { func (c *FakeCli) ResetOutputBuffers() {
c.outBuffer.Reset() c.outBuffer.Reset()
c.err.Reset() c.errBuffer.Reset()
} }
// SetNotaryClient sets the internal getter for retrieving a NotaryClient // SetNotaryClient sets the internal getter for retrieving a NotaryClient