diff --git a/cli/command/cli.go b/cli/command/cli.go index 5da0f01ddc..33eeb10949 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -19,12 +19,15 @@ import ( cliflags "github.com/docker/cli/cli/flags" manifeststore "github.com/docker/cli/cli/manifest/store" registryclient "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" + "github.com/docker/cli/internal/containerizedengine" dopts "github.com/docker/cli/opts" clitypes "github.com/docker/cli/types" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/term" "github.com/docker/go-connections/tlsconfig" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -35,18 +38,19 @@ import ( // Streams is an interface which exposes the standard input and output streams type Streams interface { - In() *InStream - Out() *OutStream + In() *streams.In + Out() *streams.Out Err() io.Writer } // Cli represents the docker command line client. type Cli interface { Client() client.APIClient - Out() *OutStream + Out() *streams.Out Err() io.Writer - In() *InStream - SetIn(in *InStream) + In() *streams.In + SetIn(in *streams.In) + Apply(ops ...DockerCliOption) error ConfigFile() *configfile.ConfigFile ServerInfo() ServerInfo ClientInfo() ClientInfo @@ -66,8 +70,8 @@ type Cli interface { // Instances of the client can be returned from NewDockerCli. type DockerCli struct { configFile *configfile.ConfigFile - in *InStream - out *OutStream + in *streams.In + out *streams.Out err io.Writer client client.APIClient serverInfo ServerInfo @@ -96,7 +100,7 @@ func (cli *DockerCli) Client() client.APIClient { } // Out returns the writer used for stdout -func (cli *DockerCli) Out() *OutStream { +func (cli *DockerCli) Out() *streams.Out { return cli.out } @@ -106,12 +110,12 @@ func (cli *DockerCli) Err() io.Writer { } // SetIn sets the reader used for stdin -func (cli *DockerCli) SetIn(in *InStream) { +func (cli *DockerCli) SetIn(in *streams.In) { cli.in = in } // In returns the reader used for stdin -func (cli *DockerCli) In() *InStream { +func (cli *DockerCli) In() *streams.In { return cli.in } @@ -393,6 +397,16 @@ func (cli *DockerCli) DockerEndpoint() docker.Endpoint { return cli.dockerEndpoint } +// Apply all the operation on the cli +func (cli *DockerCli) Apply(ops ...DockerCliOption) error { + for _, op := range ops { + if err := op(cli); err != nil { + return err + } + } + return nil +} + // ServerInfo stores details about the supported features and platform of the // server type ServerInfo struct { @@ -407,9 +421,32 @@ type ClientInfo struct { DefaultVersion string } -// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. -func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli { - return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn} +// NewDockerCli returns a DockerCli instance with all operators applied on it. +// It applies by default the standard streams, the content trust from +// environment and the default containerized client constructor operations. +func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) { + cli := &DockerCli{} + defaultOps := []DockerCliOption{ + WithContentTrustFromEnv(), + WithContainerizedClient(containerizedengine.NewClient), + } + ops = append(defaultOps, ops...) + if err := cli.Apply(ops...); err != nil { + return nil, err + } + if cli.out == nil || cli.in == nil || cli.err == nil { + stdin, stdout, stderr := term.StdStreams() + if cli.in == nil { + cli.in = streams.NewIn(stdin) + } + if cli.out == nil { + cli.out = streams.NewOut(stdout) + } + if cli.err == nil { + cli.err = stderr + } + } + return cli, nil } func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) { diff --git a/cli/command/cli_options.go b/cli/command/cli_options.go new file mode 100644 index 0000000000..43db626211 --- /dev/null +++ b/cli/command/cli_options.go @@ -0,0 +1,89 @@ +package command + +import ( + "io" + "os" + "strconv" + + "github.com/docker/cli/cli/streams" + clitypes "github.com/docker/cli/types" + "github.com/docker/docker/pkg/term" +) + +// DockerCliOption applies a modification on a DockerCli. +type DockerCliOption func(cli *DockerCli) error + +// WithStandardStreams sets a cli in, out and err streams with the standard streams. +func WithStandardStreams() DockerCliOption { + return func(cli *DockerCli) error { + // Set terminal emulation based on platform as required. + stdin, stdout, stderr := term.StdStreams() + cli.in = streams.NewIn(stdin) + cli.out = streams.NewOut(stdout) + cli.err = stderr + return nil + } +} + +// WithCombinedStreams uses the same stream for the output and error streams. +func WithCombinedStreams(combined io.Writer) DockerCliOption { + return func(cli *DockerCli) error { + cli.out = streams.NewOut(combined) + cli.err = combined + return nil + } +} + +// WithInputStream sets a cli input stream. +func WithInputStream(in io.ReadCloser) DockerCliOption { + return func(cli *DockerCli) error { + cli.in = streams.NewIn(in) + return nil + } +} + +// WithOutputStream sets a cli output stream. +func WithOutputStream(out io.Writer) DockerCliOption { + return func(cli *DockerCli) error { + cli.out = streams.NewOut(out) + return nil + } +} + +// WithErrorStream sets a cli error stream. +func WithErrorStream(err io.Writer) DockerCliOption { + return func(cli *DockerCli) error { + cli.err = err + return nil + } +} + +// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value. +func WithContentTrustFromEnv() DockerCliOption { + return func(cli *DockerCli) error { + cli.contentTrust = false + if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { + if t, err := strconv.ParseBool(e); t || err != nil { + // treat any other value as true + cli.contentTrust = true + } + } + return nil + } +} + +// WithContentTrust enables content trust on a cli. +func WithContentTrust(enabled bool) DockerCliOption { + return func(cli *DockerCli) error { + cli.contentTrust = enabled + return nil + } +} + +// WithContainerizedClient sets the containerized client constructor on a cli. +func WithContainerizedClient(containerizedFn func(string) (clitypes.ContainerizedClient, error)) DockerCliOption { + return func(cli *DockerCli) error { + cli.newContainerizeClient = containerizedFn + return nil + } +} diff --git a/cli/command/cli_test.go b/cli/command/cli_test.go index 71029e9107..e83941ffc6 100644 --- a/cli/command/cli_test.go +++ b/cli/command/cli_test.go @@ -1,8 +1,11 @@ package command import ( + "bytes" "context" "crypto/x509" + "fmt" + "io/ioutil" "os" "runtime" "testing" @@ -10,6 +13,7 @@ import ( cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/flags" + clitypes "github.com/docker/cli/types" "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/client" @@ -247,3 +251,44 @@ func TestGetClientWithPassword(t *testing.T) { }) } } + +func TestNewDockerCliAndOperators(t *testing.T) { + // Test default operations and also overriding default ones + cli, err := NewDockerCli( + WithContentTrust(true), + WithContainerizedClient(func(string) (clitypes.ContainerizedClient, error) { return nil, nil }), + ) + assert.NilError(t, err) + // Check streams are initialized + assert.Check(t, cli.In() != nil) + assert.Check(t, cli.Out() != nil) + assert.Check(t, cli.Err() != nil) + assert.Equal(t, cli.ContentTrustEnabled(), true) + client, err := cli.NewContainerizedEngineClient("") + assert.NilError(t, err) + assert.Equal(t, client, nil) + + // Apply can modify a dockerCli after construction + inbuf := bytes.NewBuffer([]byte("input")) + outbuf := bytes.NewBuffer(nil) + errbuf := bytes.NewBuffer(nil) + cli.Apply( + WithInputStream(ioutil.NopCloser(inbuf)), + WithOutputStream(outbuf), + WithErrorStream(errbuf), + ) + // Check input stream + inputStream, err := ioutil.ReadAll(cli.In()) + assert.NilError(t, err) + assert.Equal(t, string(inputStream), "input") + // Check output stream + fmt.Fprintf(cli.Out(), "output") + outputStream, err := ioutil.ReadAll(outbuf) + assert.NilError(t, err) + assert.Equal(t, string(outputStream), "output") + // Check error stream + fmt.Fprintf(cli.Err(), "error") + errStream, err := ioutil.ReadAll(errbuf) + assert.NilError(t, err) + assert.Equal(t, string(errStream), "error") +} diff --git a/cli/command/context/export-import_test.go b/cli/command/context/export-import_test.go index aac9beddeb..94d8e6aafe 100644 --- a/cli/command/context/export-import_test.go +++ b/cli/command/context/export-import_test.go @@ -8,7 +8,7 @@ import ( "path/filepath" "testing" - "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "gotest.tools/assert" ) @@ -53,7 +53,7 @@ func TestExportImportPipe(t *testing.T) { dest: "-", })) assert.Equal(t, cli.ErrBuffer().String(), "") - cli.SetIn(command.NewInStream(ioutil.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes())))) + cli.SetIn(streams.NewIn(ioutil.NopCloser(bytes.NewBuffer(cli.OutBuffer().Bytes())))) cli.OutBuffer().Reset() cli.ErrBuffer().Reset() assert.NilError(t, runImport(cli, "test2", "-")) diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index abd5a7aa8e..402aa3ae1a 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -13,7 +13,7 @@ import ( "sort" "testing" - "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/archive" @@ -39,7 +39,7 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { FROM alpine:3.6 COPY foo / `) - cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile))) + cli.SetIn(streams.NewIn(ioutil.NopCloser(dockerfile))) dir := fs.NewDir(t, t.Name(), fs.WithFile("foo", "some content")) diff --git a/cli/command/image/trust.go b/cli/command/image/trust.go index 3278fa815c..75f3ab1ccc 100644 --- a/cli/command/image/trust.go +++ b/cli/command/image/trust.go @@ -10,6 +10,7 @@ import ( "sort" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" @@ -296,7 +297,7 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru out := cli.Out() if opts.quiet { - out = command.NewOutStream(ioutil.Discard) + out = streams.NewOut(ioutil.Discard) } return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil) } diff --git a/cli/command/out.go b/cli/command/out.go deleted file mode 100644 index 89cc5d3aa1..0000000000 --- a/cli/command/out.go +++ /dev/null @@ -1,50 +0,0 @@ -package command - -import ( - "io" - "os" - - "github.com/docker/docker/pkg/term" - "github.com/sirupsen/logrus" -) - -// OutStream is an output stream used by the DockerCli to write normal program -// output. -type OutStream struct { - CommonStream - out io.Writer -} - -func (o *OutStream) Write(p []byte) (int, error) { - return o.out.Write(p) -} - -// SetRawTerminal sets raw mode on the input terminal -func (o *OutStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !o.CommonStream.isTerminal { - return nil - } - o.CommonStream.state, err = term.SetRawTerminalOutput(o.CommonStream.fd) - return err -} - -// GetTtySize returns the height and width in characters of the tty -func (o *OutStream) GetTtySize() (uint, uint) { - if !o.isTerminal { - return 0, 0 - } - ws, err := term.GetWinsize(o.fd) - if err != nil { - logrus.Debugf("Error getting size: %s", err) - if ws == nil { - return 0, 0 - } - } - return uint(ws.Height), uint(ws.Width) -} - -// NewOutStream returns a new OutStream object from a Writer -func NewOutStream(out io.Writer) *OutStream { - fd, isTerminal := term.GetFdInfo(out) - return &OutStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, out: out} -} diff --git a/cli/command/registry.go b/cli/command/registry.go index c12843693e..f0276680bd 100644 --- a/cli/command/registry.go +++ b/cli/command/registry.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/docker/cli/cli/debug" + "github.com/docker/cli/cli/streams" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" @@ -101,7 +102,7 @@ func GetDefaultAuthConfig(cli Cli, checkCredStore bool, serverAddress string, is func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *types.AuthConfig, isDefaultRegistry bool) error { // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 if runtime.GOOS == "windows" { - cli.SetIn(NewInStream(os.Stdin)) + cli.SetIn(streams.NewIn(os.Stdin)) } // Some links documenting this: diff --git a/cli/command/stack/kubernetes/deploy.go b/cli/command/stack/kubernetes/deploy.go index 638daddd2f..84fdc638c2 100644 --- a/cli/command/stack/kubernetes/deploy.go +++ b/cli/command/stack/kubernetes/deploy.go @@ -4,9 +4,9 @@ import ( "fmt" "io" - "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/stack/options" composetypes "github.com/docker/cli/cli/compose/types" + "github.com/docker/cli/cli/streams" "github.com/morikuni/aec" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" ) @@ -117,7 +117,7 @@ func metaStateFromStatus(status serviceStatus) metaServiceState { } type forwardOnlyStatusDisplay struct { - o *command.OutStream + o *streams.Out states map[string]metaServiceState } @@ -130,7 +130,7 @@ func (d *forwardOnlyStatusDisplay) OnStatus(status serviceStatus) { } type interactiveStatusDisplay struct { - o *command.OutStream + o *streams.Out statuses []serviceStatus } @@ -163,7 +163,7 @@ func displayInteractiveServiceStatus(status serviceStatus, o io.Writer) { status.podsReady, status.podsPending, totalFailed, status.podsTotal) } -func newStatusDisplay(o *command.OutStream) statusDisplay { +func newStatusDisplay(o *streams.Out) statusDisplay { if !o.IsTerminal() { return &forwardOnlyStatusDisplay{o: o, states: map[string]metaServiceState{}} } diff --git a/cli/command/streams.go b/cli/command/streams.go new file mode 100644 index 0000000000..fa435e1643 --- /dev/null +++ b/cli/command/streams.go @@ -0,0 +1,23 @@ +package command + +import ( + "github.com/docker/cli/cli/streams" +) + +// InStream is an input stream used by the DockerCli to read user input +// Deprecated: Use github.com/docker/cli/cli/streams.In instead +type InStream = streams.In + +// OutStream is an output stream used by the DockerCli to write normal program +// output. +// Deprecated: Use github.com/docker/cli/cli/streams.Out instead +type OutStream = streams.Out + +var ( + // NewInStream returns a new InStream object from a ReadCloser + // Deprecated: Use github.com/docker/cli/cli/streams.NewIn instead + NewInStream = streams.NewIn + // NewOutStream returns a new OutStream object from a Writer + // Deprecated: Use github.com/docker/cli/cli/streams.NewOut instead + NewOutStream = streams.NewOut +) diff --git a/cli/command/swarm/unlock.go b/cli/command/swarm/unlock.go index 7d0dce68ae..35f995bd5b 100644 --- a/cli/command/swarm/unlock.go +++ b/cli/command/swarm/unlock.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -59,7 +60,7 @@ func runUnlock(dockerCli command.Cli) error { return client.SwarmUnlock(ctx, req) } -func readKey(in *command.InStream, prompt string) (string, error) { +func readKey(in *streams.In, prompt string) (string, error) { if in.IsTerminal() { fmt.Print(prompt) dt, err := terminal.ReadPassword(int(in.FD())) diff --git a/cli/command/swarm/unlock_test.go b/cli/command/swarm/unlock_test.go index 8eb2ecd4f0..f576dc34cf 100644 --- a/cli/command/swarm/unlock_test.go +++ b/cli/command/swarm/unlock_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" @@ -92,7 +92,7 @@ func TestSwarmUnlock(t *testing.T) { return nil }, }) - dockerCli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) + dockerCli.SetIn(streams.NewIn(ioutil.NopCloser(strings.NewReader(input)))) cmd := newUnlockCommand(dockerCli) assert.NilError(t, cmd.Execute()) } diff --git a/cli/command/utils.go b/cli/command/utils.go index 13954c0101..26f53814cd 100644 --- a/cli/command/utils.go +++ b/cli/command/utils.go @@ -9,6 +9,7 @@ import ( "runtime" "strings" + "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/pkg/system" "github.com/spf13/pflag" @@ -80,7 +81,7 @@ func PromptForConfirmation(ins io.Reader, outs io.Writer, message string) bool { // On Windows, force the use of the regular OS stdin stream. if runtime.GOOS == "windows" { - ins = NewInStream(os.Stdin) + ins = streams.NewIn(os.Stdin) } reader := bufio.NewReader(ins) diff --git a/cli/command/volume/prune_test.go b/cli/command/volume/prune_test.go index 79d4d52e32..800eaa730f 100644 --- a/cli/command/volume/prune_test.go +++ b/cli/command/volume/prune_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -86,7 +86,7 @@ func TestVolumePrunePromptYes(t *testing.T) { volumePruneFunc: simplePruneFunc, }) - cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) + cli.SetIn(streams.NewIn(ioutil.NopCloser(strings.NewReader(input)))) cmd := NewPruneCommand(cli) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden") @@ -102,7 +102,7 @@ func TestVolumePrunePromptNo(t *testing.T) { volumePruneFunc: simplePruneFunc, }) - cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) + cli.SetIn(streams.NewIn(ioutil.NopCloser(strings.NewReader(input)))) cmd := NewPruneCommand(cli) assert.NilError(t, cmd.Execute()) golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden") diff --git a/cli/command/in.go b/cli/streams/in.go similarity index 59% rename from cli/command/in.go rename to cli/streams/in.go index 54855c6dc2..931c434e37 100644 --- a/cli/command/in.go +++ b/cli/streams/in.go @@ -1,4 +1,4 @@ -package command +package streams import ( "errors" @@ -9,33 +9,33 @@ import ( "github.com/docker/docker/pkg/term" ) -// InStream is an input stream used by the DockerCli to read user input -type InStream struct { - CommonStream +// In is an input stream used by the DockerCli to read user input +type In struct { + commonStream in io.ReadCloser } -func (i *InStream) Read(p []byte) (int, error) { +func (i *In) Read(p []byte) (int, error) { return i.in.Read(p) } // Close implements the Closer interface -func (i *InStream) Close() error { +func (i *In) Close() error { return i.in.Close() } // SetRawTerminal sets raw mode on the input terminal -func (i *InStream) SetRawTerminal() (err error) { - if os.Getenv("NORAW") != "" || !i.CommonStream.isTerminal { +func (i *In) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !i.commonStream.isTerminal { return nil } - i.CommonStream.state, err = term.SetRawTerminal(i.CommonStream.fd) + i.commonStream.state, err = term.SetRawTerminal(i.commonStream.fd) return err } // CheckTty checks if we are trying to attach to a container tty // from a non-tty client input stream, and if so, returns an error. -func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { +func (i *In) CheckTty(attachStdin, ttyMode bool) error { // In order to attach to a container tty, input stream for the client must // be a tty itself: redirecting or piping the client standard input is // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. @@ -49,8 +49,8 @@ func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { return nil } -// NewInStream returns a new InStream object from a ReadCloser -func NewInStream(in io.ReadCloser) *InStream { +// NewIn returns a new In object from a ReadCloser +func NewIn(in io.ReadCloser) *In { fd, isTerminal := term.GetFdInfo(in) - return &InStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, in: in} + return &In{commonStream: commonStream{fd: fd, isTerminal: isTerminal}, in: in} } diff --git a/cli/streams/out.go b/cli/streams/out.go new file mode 100644 index 0000000000..036f493771 --- /dev/null +++ b/cli/streams/out.go @@ -0,0 +1,50 @@ +package streams + +import ( + "io" + "os" + + "github.com/docker/docker/pkg/term" + "github.com/sirupsen/logrus" +) + +// Out is an output stream used by the DockerCli to write normal program +// output. +type Out struct { + commonStream + out io.Writer +} + +func (o *Out) Write(p []byte) (int, error) { + return o.out.Write(p) +} + +// SetRawTerminal sets raw mode on the input terminal +func (o *Out) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !o.commonStream.isTerminal { + return nil + } + o.commonStream.state, err = term.SetRawTerminalOutput(o.commonStream.fd) + return err +} + +// GetTtySize returns the height and width in characters of the tty +func (o *Out) GetTtySize() (uint, uint) { + if !o.isTerminal { + return 0, 0 + } + ws, err := term.GetWinsize(o.fd) + if err != nil { + logrus.Debugf("Error getting size: %s", err) + if ws == nil { + return 0, 0 + } + } + return uint(ws.Height), uint(ws.Width) +} + +// NewOut returns a new Out object from a Writer +func NewOut(out io.Writer) *Out { + fd, isTerminal := term.GetFdInfo(out) + return &Out{commonStream: commonStream{fd: fd, isTerminal: isTerminal}, out: out} +} diff --git a/cli/command/stream.go b/cli/streams/stream.go similarity index 63% rename from cli/command/stream.go rename to cli/streams/stream.go index 71a43fa2e9..f97bc69fdd 100644 --- a/cli/command/stream.go +++ b/cli/streams/stream.go @@ -1,34 +1,34 @@ -package command +package streams import ( "github.com/docker/docker/pkg/term" ) -// CommonStream is an input stream used by the DockerCli to read user input -type CommonStream struct { +// commonStream is an input stream used by the DockerCli to read user input +type commonStream struct { fd uintptr isTerminal bool state *term.State } // FD returns the file descriptor number for this stream -func (s *CommonStream) FD() uintptr { +func (s *commonStream) FD() uintptr { return s.fd } // IsTerminal returns true if this stream is connected to a terminal -func (s *CommonStream) IsTerminal() bool { +func (s *commonStream) IsTerminal() bool { return s.isTerminal } // RestoreTerminal restores normal mode to the terminal -func (s *CommonStream) RestoreTerminal() { +func (s *commonStream) RestoreTerminal() { if s.state != nil { term.RestoreTerminal(s.fd, s.state) } } // SetIsTerminal sets the boolean used for isTerminal -func (s *CommonStream) SetIsTerminal(isTerminal bool) { +func (s *commonStream) SetIsTerminal(isTerminal bool) { s.isTerminal = isTerminal } diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index ab31ad499e..5909953be3 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "strconv" "strings" "github.com/docker/cli/cli" @@ -13,10 +12,8 @@ import ( cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/debug" cliflags "github.com/docker/cli/cli/flags" - "github.com/docker/cli/internal/containerizedengine" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" - "github.com/docker/docker/pkg/term" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -170,17 +167,19 @@ func noArgs(cmd *cobra.Command, args []string) error { } func main() { - // Set terminal emulation based on platform as required. - stdin, stdout, stderr := term.StdStreams() - logrus.SetOutput(stderr) + dockerCli, err := command.NewDockerCli() + if err != nil { + fmt.Fprintln(dockerCli.Err(), err) + os.Exit(1) + } + logrus.SetOutput(dockerCli.Err()) - dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled(), containerizedengine.NewClient) cmd := newDockerCommand(dockerCli) if err := cmd.Execute(); err != nil { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { - fmt.Fprintln(stderr, sterr.Status) + fmt.Fprintln(dockerCli.Err(), sterr.Status) } // StatusError should only be used for errors, and all errors should // have a non-zero exit status, so never exit with 0 @@ -189,21 +188,11 @@ func main() { } os.Exit(sterr.StatusCode) } - fmt.Fprintln(stderr, err) + fmt.Fprintln(dockerCli.Err(), err) os.Exit(1) } } -func contentTrustEnabled() bool { - if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { - if t, err := strconv.ParseBool(e); t || err != nil { - // treat any other value as true - return true - } - } - return false -} - func dockerPreRun(opts *cliflags.ClientOptions) { cliflags.SetLogLevel(opts.Common.LogLevel) diff --git a/cmd/docker/docker_test.go b/cmd/docker/docker_test.go index 7c6e4526f4..e0f23747c0 100644 --- a/cmd/docker/docker_test.go +++ b/cmd/docker/docker_test.go @@ -25,27 +25,33 @@ func TestClientDebugEnabled(t *testing.T) { assert.Check(t, is.Equal(logrus.DebugLevel, logrus.GetLevel())) } +var discard = ioutil.NopCloser(bytes.NewBuffer(nil)) + func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) { - discard := ioutil.Discard - cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil)) + cli, err := command.NewDockerCli(command.WithInputStream(discard), command.WithCombinedStreams(ioutil.Discard)) + assert.NilError(t, err) + cmd := newDockerCommand(cli) cmd.SetArgs([]string{"help", "invalid"}) - err := cmd.Execute() + err = cmd.Execute() assert.Error(t, err, "unknown help topic: invalid") } func TestExitStatusForInvalidSubcommand(t *testing.T) { - discard := ioutil.Discard - cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil)) + cli, err := command.NewDockerCli(command.WithInputStream(discard), command.WithCombinedStreams(ioutil.Discard)) + assert.NilError(t, err) + cmd := newDockerCommand(cli) cmd.SetArgs([]string{"invalid"}) - err := cmd.Execute() + err = cmd.Execute() assert.Check(t, is.ErrorContains(err, "docker: 'invalid' is not a docker command.")) } func TestVersion(t *testing.T) { var b bytes.Buffer - cmd := newDockerCommand(command.NewDockerCli(os.Stdin, &b, &b, false, nil)) + cli, err := command.NewDockerCli(command.WithInputStream(discard), command.WithCombinedStreams(&b)) + assert.NilError(t, err) + cmd := newDockerCommand(cli) cmd.SetArgs([]string{"--version"}) - err := cmd.Execute() + err = cmd.Execute() assert.NilError(t, err) assert.Check(t, is.Contains(b.String(), "Docker version")) } diff --git a/docs/yaml/generate.go b/docs/yaml/generate.go index 09550b2fad..fdc5a2522f 100644 --- a/docs/yaml/generate.go +++ b/docs/yaml/generate.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/commands" - "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -18,8 +17,10 @@ import ( const descriptionSourcePath = "docs/reference/commandline/" func generateCliYaml(opts *options) error { - stdin, stdout, stderr := term.StdStreams() - dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil) + dockerCli, err := command.NewDockerCli() + if err != nil { + return err + } cmd := &cobra.Command{Use: "docker"} commands.AddCommands(cmd, dockerCli) disableFlagsInUseLine(cmd) diff --git a/internal/containerizedengine/containerd_test.go b/internal/containerizedengine/containerd_test.go index cca86be55b..0a724a4924 100644 --- a/internal/containerizedengine/containerd_test.go +++ b/internal/containerizedengine/containerd_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/containerd/containerd" - "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types" "gotest.tools/assert" ) @@ -24,7 +24,7 @@ func TestPullWithAuthPullFail(t *testing.T) { } imageName := "testnamegoeshere" - _, err := client.pullWithAuth(ctx, imageName, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + _, err := client.pullWithAuth(ctx, imageName, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "pull failure") } @@ -40,6 +40,6 @@ func TestPullWithAuthPullPass(t *testing.T) { } imageName := "testnamegoeshere" - _, err := client.pullWithAuth(ctx, imageName, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + _, err := client.pullWithAuth(ctx, imageName, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.NilError(t, err) } diff --git a/internal/containerizedengine/update_test.go b/internal/containerizedengine/update_test.go index 25ad7ab5a9..24b51d9f53 100644 --- a/internal/containerizedengine/update_test.go +++ b/internal/containerizedengine/update_test.go @@ -11,7 +11,7 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/cio" "github.com/containerd/containerd/errdefs" - "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/internal/versions" clitypes "github.com/docker/cli/types" "github.com/docker/docker/api/types" @@ -45,21 +45,21 @@ func TestActivateImagePermutations(t *testing.T) { RuntimeMetadataDir: tmpdir, } - err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.ActivateEngine(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, expectedError.Error()) assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion)) metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage} err = versions.WriteRuntimeMetadata(tmpdir, &metadata) assert.NilError(t, err) - err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.ActivateEngine(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, expectedError.Error()) assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion)) metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage + "-dm"} err = versions.WriteRuntimeMetadata(tmpdir, &metadata) assert.NilError(t, err) - err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.ActivateEngine(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, expectedError.Error()) assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage+"-dm", opts.EngineVersion)) } @@ -110,7 +110,7 @@ func TestActivateConfigFailure(t *testing.T) { RuntimeMetadataDir: tmpdir, } - err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.ActivateEngine(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "config lookup failure") } @@ -152,7 +152,7 @@ func TestActivateDoUpdateFail(t *testing.T) { RuntimeMetadataDir: tmpdir, } - err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.ActivateEngine(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "check for image") assert.ErrorContains(t, err, "something went wrong") } @@ -174,7 +174,7 @@ func TestDoUpdateNoVersion(t *testing.T) { } client := baseClient{} - err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.DoUpdate(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "pick the version you") } @@ -202,7 +202,7 @@ func TestDoUpdateImageMiscError(t *testing.T) { }, } - err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.DoUpdate(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "check for image") assert.ErrorContains(t, err, "something went wrong") } @@ -234,7 +234,7 @@ func TestDoUpdatePullFail(t *testing.T) { }, } - err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.DoUpdate(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "unable to pull") assert.ErrorContains(t, err, "pull failure") } @@ -280,7 +280,7 @@ func TestActivateDoUpdateVerifyImageName(t *testing.T) { RuntimeMetadataDir: tmpdir, } - err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}) + err = client.ActivateEngine(ctx, opts, streams.NewOut(&bytes.Buffer{}), &types.AuthConfig{}) assert.ErrorContains(t, err, "check for image") assert.ErrorContains(t, err, "something went wrong") expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion) diff --git a/internal/test/cli.go b/internal/test/cli.go index 164488ca2e..12b8b621d9 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -13,6 +13,7 @@ import ( "github.com/docker/cli/cli/context/store" manifeststore "github.com/docker/cli/cli/manifest/store" registryclient "github.com/docker/cli/cli/registry/client" + "github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/trust" clitypes "github.com/docker/cli/types" "github.com/docker/docker/client" @@ -29,10 +30,10 @@ type FakeCli struct { command.DockerCli client client.APIClient configfile *configfile.ConfigFile - out *command.OutStream + out *streams.Out outBuffer *bytes.Buffer err *bytes.Buffer - in *command.InStream + in *streams.In server command.ServerInfo clientInfoFunc clientInfoFuncType notaryClientFunc NotaryClientFuncType @@ -51,10 +52,10 @@ func NewFakeCli(client client.APIClient, opts ...func(*FakeCli)) *FakeCli { errBuffer := new(bytes.Buffer) c := &FakeCli{ client: client, - out: command.NewOutStream(outBuffer), + out: streams.NewOut(outBuffer), outBuffer: outBuffer, err: errBuffer, - in: command.NewInStream(ioutil.NopCloser(strings.NewReader(""))), + in: streams.NewIn(ioutil.NopCloser(strings.NewReader(""))), // Use an empty string for filename so that tests don't create configfiles // Set cli.ConfigFile().Filename to a tempfile to support Save. configfile: configfile.New(""), @@ -66,7 +67,7 @@ func NewFakeCli(client client.APIClient, opts ...func(*FakeCli)) *FakeCli { } // SetIn sets the input of the cli to the specified ReadCloser -func (c *FakeCli) SetIn(in *command.InStream) { +func (c *FakeCli) SetIn(in *streams.In) { c.in = in } @@ -76,7 +77,7 @@ func (c *FakeCli) SetErr(err *bytes.Buffer) { } // SetOut sets the stdout stream for the cli to the specified io.Writer -func (c *FakeCli) SetOut(out *command.OutStream) { +func (c *FakeCli) SetOut(out *streams.Out) { c.out = out } @@ -106,7 +107,7 @@ func (c *FakeCli) Client() client.APIClient { } // Out returns the output stream (stdout) the cli should write on -func (c *FakeCli) Out() *command.OutStream { +func (c *FakeCli) Out() *streams.Out { return c.out } @@ -116,7 +117,7 @@ func (c *FakeCli) Err() io.Writer { } // In returns the input stream the cli will use -func (c *FakeCli) In() *command.InStream { +func (c *FakeCli) In() *streams.In { return c.in } diff --git a/man/generate.go b/man/generate.go index e5e480be3f..63c4219d79 100644 --- a/man/generate.go +++ b/man/generate.go @@ -11,7 +11,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/commands" - "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/pflag" @@ -37,8 +36,10 @@ func generateManPages(opts *options) error { header.Date = &now } - stdin, stdout, stderr := term.StdStreams() - dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil) + dockerCli, err := command.NewDockerCli() + if err != nil { + return err + } cmd := &cobra.Command{Use: "docker"} commands.AddCommands(cmd, dockerCli) source := filepath.Join(opts.source, descriptionSourcePath)