Merge pull request #1633 from silvin-lubecki/refactor-docker-cli-construction

Introduce functional arguments to NewDockerCli for a more stable API.
This commit is contained in:
Vincent Demeester 2019-01-28 15:39:34 +01:00 committed by GitHub
commit f95ca8e1ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 350 additions and 154 deletions

View File

@ -19,12 +19,15 @@ import (
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store" manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client" registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/containerizedengine"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
clitypes "github.com/docker/cli/types" clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/term"
"github.com/docker/go-connections/tlsconfig" "github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -35,18 +38,19 @@ import (
// Streams is an interface which exposes the standard input and output streams // Streams is an interface which exposes the standard input and output streams
type Streams interface { type Streams interface {
In() *InStream In() *streams.In
Out() *OutStream Out() *streams.Out
Err() io.Writer Err() io.Writer
} }
// Cli represents the docker command line client. // Cli represents the docker command line client.
type Cli interface { type Cli interface {
Client() client.APIClient Client() client.APIClient
Out() *OutStream Out() *streams.Out
Err() io.Writer Err() io.Writer
In() *InStream In() *streams.In
SetIn(in *InStream) SetIn(in *streams.In)
Apply(ops ...DockerCliOption) error
ConfigFile() *configfile.ConfigFile ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo ServerInfo() ServerInfo
ClientInfo() ClientInfo ClientInfo() ClientInfo
@ -66,8 +70,8 @@ type Cli interface {
// Instances of the client can be returned from NewDockerCli. // Instances of the client can be returned from NewDockerCli.
type DockerCli struct { type DockerCli struct {
configFile *configfile.ConfigFile configFile *configfile.ConfigFile
in *InStream in *streams.In
out *OutStream out *streams.Out
err io.Writer err io.Writer
client client.APIClient client client.APIClient
serverInfo ServerInfo serverInfo ServerInfo
@ -96,7 +100,7 @@ func (cli *DockerCli) Client() client.APIClient {
} }
// Out returns the writer used for stdout // Out returns the writer used for stdout
func (cli *DockerCli) Out() *OutStream { func (cli *DockerCli) Out() *streams.Out {
return cli.out return cli.out
} }
@ -106,12 +110,12 @@ func (cli *DockerCli) Err() io.Writer {
} }
// SetIn sets the reader used for stdin // SetIn sets the reader used for stdin
func (cli *DockerCli) SetIn(in *InStream) { func (cli *DockerCli) SetIn(in *streams.In) {
cli.in = in cli.in = in
} }
// In returns the reader used for stdin // In returns the reader used for stdin
func (cli *DockerCli) In() *InStream { func (cli *DockerCli) In() *streams.In {
return cli.in return cli.in
} }
@ -393,6 +397,16 @@ func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
return cli.dockerEndpoint 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 // ServerInfo stores details about the supported features and platform of the
// server // server
type ServerInfo struct { type ServerInfo struct {
@ -407,9 +421,32 @@ type ClientInfo struct {
DefaultVersion string DefaultVersion string
} }
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. // NewDockerCli returns a DockerCli instance with all operators applied on it.
func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli { // It applies by default the standard streams, the content trust from
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn} // 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) { func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {

View File

@ -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
}
}

View File

@ -1,8 +1,11 @@
package command package command
import ( import (
"bytes"
"context" "context"
"crypto/x509" "crypto/x509"
"fmt"
"io/ioutil"
"os" "os"
"runtime" "runtime"
"testing" "testing"
@ -10,6 +13,7 @@ import (
cliconfig "github.com/docker/cli/cli/config" cliconfig "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"
clitypes "github.com/docker/cli/types"
"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"
@ -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")
}

View File

@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams"
"gotest.tools/assert" "gotest.tools/assert"
) )
@ -53,7 +53,7 @@ func TestExportImportPipe(t *testing.T) {
dest: "-", dest: "-",
})) }))
assert.Equal(t, cli.ErrBuffer().String(), "") 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.OutBuffer().Reset()
cli.ErrBuffer().Reset() cli.ErrBuffer().Reset()
assert.NilError(t, runImport(cli, "test2", "-")) assert.NilError(t, runImport(cli, "test2", "-"))

View File

@ -13,7 +13,7 @@ import (
"sort" "sort"
"testing" "testing"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
@ -39,7 +39,7 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
FROM alpine:3.6 FROM alpine:3.6
COPY foo / COPY foo /
`) `)
cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile))) cli.SetIn(streams.NewIn(ioutil.NopCloser(dockerfile)))
dir := fs.NewDir(t, t.Name(), dir := fs.NewDir(t, t.Name(),
fs.WithFile("foo", "some content")) fs.WithFile("foo", "some content"))

View File

@ -10,6 +10,7 @@ import (
"sort" "sort"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -296,7 +297,7 @@ func imagePullPrivileged(ctx context.Context, cli command.Cli, imgRefAndAuth tru
out := cli.Out() out := cli.Out()
if opts.quiet { if opts.quiet {
out = command.NewOutStream(ioutil.Discard) out = streams.NewOut(ioutil.Discard)
} }
return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil) return jsonmessage.DisplayJSONMessagesToStream(responseBody, out, nil)
} }

View File

@ -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}
}

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
"github.com/docker/cli/cli/streams"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry" 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 { 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 // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
cli.SetIn(NewInStream(os.Stdin)) cli.SetIn(streams.NewIn(os.Stdin))
} }
// Some links documenting this: // Some links documenting this:

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/options" "github.com/docker/cli/cli/command/stack/options"
composetypes "github.com/docker/cli/cli/compose/types" composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/cli/streams"
"github.com/morikuni/aec" "github.com/morikuni/aec"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
) )
@ -117,7 +117,7 @@ func metaStateFromStatus(status serviceStatus) metaServiceState {
} }
type forwardOnlyStatusDisplay struct { type forwardOnlyStatusDisplay struct {
o *command.OutStream o *streams.Out
states map[string]metaServiceState states map[string]metaServiceState
} }
@ -130,7 +130,7 @@ func (d *forwardOnlyStatusDisplay) OnStatus(status serviceStatus) {
} }
type interactiveStatusDisplay struct { type interactiveStatusDisplay struct {
o *command.OutStream o *streams.Out
statuses []serviceStatus statuses []serviceStatus
} }
@ -163,7 +163,7 @@ func displayInteractiveServiceStatus(status serviceStatus, o io.Writer) {
status.podsReady, status.podsPending, totalFailed, status.podsTotal) status.podsReady, status.podsPending, totalFailed, status.podsTotal)
} }
func newStatusDisplay(o *command.OutStream) statusDisplay { func newStatusDisplay(o *streams.Out) statusDisplay {
if !o.IsTerminal() { if !o.IsTerminal() {
return &forwardOnlyStatusDisplay{o: o, states: map[string]metaServiceState{}} return &forwardOnlyStatusDisplay{o: o, states: map[string]metaServiceState{}}
} }

23
cli/command/streams.go Normal file
View File

@ -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
)

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -59,7 +60,7 @@ func runUnlock(dockerCli command.Cli) error {
return client.SwarmUnlock(ctx, req) 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() { if in.IsTerminal() {
fmt.Print(prompt) fmt.Print(prompt)
dt, err := terminal.ReadPassword(int(in.FD())) dt, err := terminal.ReadPassword(int(in.FD()))

View File

@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
@ -92,7 +92,7 @@ func TestSwarmUnlock(t *testing.T) {
return nil return nil
}, },
}) })
dockerCli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) dockerCli.SetIn(streams.NewIn(ioutil.NopCloser(strings.NewReader(input))))
cmd := newUnlockCommand(dockerCli) cmd := newUnlockCommand(dockerCli)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
} }

View File

@ -9,6 +9,7 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/docker/docker/pkg/system" "github.com/docker/docker/pkg/system"
"github.com/spf13/pflag" "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. // On Windows, force the use of the regular OS stdin stream.
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
ins = NewInStream(os.Stdin) ins = streams.NewIn(os.Stdin)
} }
reader := bufio.NewReader(ins) reader := bufio.NewReader(ins)

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -86,7 +86,7 @@ func TestVolumePrunePromptYes(t *testing.T) {
volumePruneFunc: simplePruneFunc, volumePruneFunc: simplePruneFunc,
}) })
cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) cli.SetIn(streams.NewIn(ioutil.NopCloser(strings.NewReader(input))))
cmd := NewPruneCommand(cli) cmd := NewPruneCommand(cli)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden") golden.Assert(t, cli.OutBuffer().String(), "volume-prune-yes.golden")
@ -102,7 +102,7 @@ func TestVolumePrunePromptNo(t *testing.T) {
volumePruneFunc: simplePruneFunc, volumePruneFunc: simplePruneFunc,
}) })
cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input)))) cli.SetIn(streams.NewIn(ioutil.NopCloser(strings.NewReader(input))))
cmd := NewPruneCommand(cli) cmd := NewPruneCommand(cli)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden") golden.Assert(t, cli.OutBuffer().String(), "volume-prune-no.golden")

View File

@ -1,4 +1,4 @@
package command package streams
import ( import (
"errors" "errors"
@ -9,33 +9,33 @@ import (
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
) )
// InStream is an input stream used by the DockerCli to read user input // In is an input stream used by the DockerCli to read user input
type InStream struct { type In struct {
CommonStream commonStream
in io.ReadCloser in io.ReadCloser
} }
func (i *InStream) Read(p []byte) (int, error) { func (i *In) Read(p []byte) (int, error) {
return i.in.Read(p) return i.in.Read(p)
} }
// Close implements the Closer interface // Close implements the Closer interface
func (i *InStream) Close() error { func (i *In) Close() error {
return i.in.Close() return i.in.Close()
} }
// SetRawTerminal sets raw mode on the input terminal // SetRawTerminal sets raw mode on the input terminal
func (i *InStream) SetRawTerminal() (err error) { func (i *In) SetRawTerminal() (err error) {
if os.Getenv("NORAW") != "" || !i.CommonStream.isTerminal { if os.Getenv("NORAW") != "" || !i.commonStream.isTerminal {
return nil return nil
} }
i.CommonStream.state, err = term.SetRawTerminal(i.CommonStream.fd) i.commonStream.state, err = term.SetRawTerminal(i.commonStream.fd)
return err return err
} }
// CheckTty checks if we are trying to attach to a container tty // 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. // 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 // 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 // be a tty itself: redirecting or piping the client standard input is
// incompatible with `docker run -t`, `docker exec -t` or `docker attach`. // 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 return nil
} }
// NewInStream returns a new InStream object from a ReadCloser // NewIn returns a new In object from a ReadCloser
func NewInStream(in io.ReadCloser) *InStream { func NewIn(in io.ReadCloser) *In {
fd, isTerminal := term.GetFdInfo(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}
} }

50
cli/streams/out.go Normal file
View File

@ -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}
}

View File

@ -1,34 +1,34 @@
package command package streams
import ( import (
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
) )
// CommonStream is an input stream used by the DockerCli to read user input // commonStream is an input stream used by the DockerCli to read user input
type CommonStream struct { type commonStream struct {
fd uintptr fd uintptr
isTerminal bool isTerminal bool
state *term.State state *term.State
} }
// FD returns the file descriptor number for this stream // FD returns the file descriptor number for this stream
func (s *CommonStream) FD() uintptr { func (s *commonStream) FD() uintptr {
return s.fd return s.fd
} }
// IsTerminal returns true if this stream is connected to a terminal // IsTerminal returns true if this stream is connected to a terminal
func (s *CommonStream) IsTerminal() bool { func (s *commonStream) IsTerminal() bool {
return s.isTerminal return s.isTerminal
} }
// RestoreTerminal restores normal mode to the terminal // RestoreTerminal restores normal mode to the terminal
func (s *CommonStream) RestoreTerminal() { func (s *commonStream) RestoreTerminal() {
if s.state != nil { if s.state != nil {
term.RestoreTerminal(s.fd, s.state) term.RestoreTerminal(s.fd, s.state)
} }
} }
// SetIsTerminal sets the boolean used for isTerminal // SetIsTerminal sets the boolean used for isTerminal
func (s *CommonStream) SetIsTerminal(isTerminal bool) { func (s *commonStream) SetIsTerminal(isTerminal bool) {
s.isTerminal = isTerminal s.isTerminal = isTerminal
} }

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
@ -13,10 +12,8 @@ import (
cliconfig "github.com/docker/cli/cli/config" cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/term"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -170,17 +167,19 @@ func noArgs(cmd *cobra.Command, args []string) error {
} }
func main() { func main() {
// Set terminal emulation based on platform as required. dockerCli, err := command.NewDockerCli()
stdin, stdout, stderr := term.StdStreams() if err != nil {
logrus.SetOutput(stderr) fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1)
}
logrus.SetOutput(dockerCli.Err())
dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled(), containerizedengine.NewClient)
cmd := newDockerCommand(dockerCli) cmd := newDockerCommand(dockerCli)
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
if sterr, ok := err.(cli.StatusError); ok { if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" { 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 // StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0 // have a non-zero exit status, so never exit with 0
@ -189,21 +188,11 @@ func main() {
} }
os.Exit(sterr.StatusCode) os.Exit(sterr.StatusCode)
} }
fmt.Fprintln(stderr, err) fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1) 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) { func dockerPreRun(opts *cliflags.ClientOptions) {
cliflags.SetLogLevel(opts.Common.LogLevel) cliflags.SetLogLevel(opts.Common.LogLevel)

View File

@ -25,27 +25,33 @@ func TestClientDebugEnabled(t *testing.T) {
assert.Check(t, is.Equal(logrus.DebugLevel, logrus.GetLevel())) assert.Check(t, is.Equal(logrus.DebugLevel, logrus.GetLevel()))
} }
var discard = ioutil.NopCloser(bytes.NewBuffer(nil))
func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) { func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) {
discard := ioutil.Discard cli, err := command.NewDockerCli(command.WithInputStream(discard), command.WithCombinedStreams(ioutil.Discard))
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil)) assert.NilError(t, err)
cmd := newDockerCommand(cli)
cmd.SetArgs([]string{"help", "invalid"}) cmd.SetArgs([]string{"help", "invalid"})
err := cmd.Execute() err = cmd.Execute()
assert.Error(t, err, "unknown help topic: invalid") assert.Error(t, err, "unknown help topic: invalid")
} }
func TestExitStatusForInvalidSubcommand(t *testing.T) { func TestExitStatusForInvalidSubcommand(t *testing.T) {
discard := ioutil.Discard cli, err := command.NewDockerCli(command.WithInputStream(discard), command.WithCombinedStreams(ioutil.Discard))
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil)) assert.NilError(t, err)
cmd := newDockerCommand(cli)
cmd.SetArgs([]string{"invalid"}) cmd.SetArgs([]string{"invalid"})
err := cmd.Execute() err = cmd.Execute()
assert.Check(t, is.ErrorContains(err, "docker: 'invalid' is not a docker command.")) assert.Check(t, is.ErrorContains(err, "docker: 'invalid' is not a docker command."))
} }
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {
var b bytes.Buffer 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"}) cmd.SetArgs([]string{"--version"})
err := cmd.Execute() err = cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, is.Contains(b.String(), "Docker version")) assert.Check(t, is.Contains(b.String(), "Docker version"))
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/commands" "github.com/docker/cli/cli/command/commands"
"github.com/docker/docker/pkg/term"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@ -18,8 +17,10 @@ import (
const descriptionSourcePath = "docs/reference/commandline/" const descriptionSourcePath = "docs/reference/commandline/"
func generateCliYaml(opts *options) error { func generateCliYaml(opts *options) error {
stdin, stdout, stderr := term.StdStreams() dockerCli, err := command.NewDockerCli()
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil) if err != nil {
return err
}
cmd := &cobra.Command{Use: "docker"} cmd := &cobra.Command{Use: "docker"}
commands.AddCommands(cmd, dockerCli) commands.AddCommands(cmd, dockerCli)
disableFlagsInUseLine(cmd) disableFlagsInUseLine(cmd)

View File

@ -7,7 +7,7 @@ import (
"testing" "testing"
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"gotest.tools/assert" "gotest.tools/assert"
) )
@ -24,7 +24,7 @@ func TestPullWithAuthPullFail(t *testing.T) {
} }
imageName := "testnamegoeshere" 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") assert.ErrorContains(t, err, "pull failure")
} }
@ -40,6 +40,6 @@ func TestPullWithAuthPullPass(t *testing.T) {
} }
imageName := "testnamegoeshere" 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) assert.NilError(t, err)
} }

View File

@ -11,7 +11,7 @@ import (
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/cio" "github.com/containerd/containerd/cio"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/versions" "github.com/docker/cli/internal/versions"
clitypes "github.com/docker/cli/types" clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
@ -45,21 +45,21 @@ func TestActivateImagePermutations(t *testing.T) {
RuntimeMetadataDir: tmpdir, 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.ErrorContains(t, err, expectedError.Error())
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion)) assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion))
metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage} metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata) err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err) 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.ErrorContains(t, err, expectedError.Error())
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion)) assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion))
metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage + "-dm"} metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage + "-dm"}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata) err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err) 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.ErrorContains(t, err, expectedError.Error())
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage+"-dm", opts.EngineVersion)) 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, 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") assert.ErrorContains(t, err, "config lookup failure")
} }
@ -152,7 +152,7 @@ func TestActivateDoUpdateFail(t *testing.T) {
RuntimeMetadataDir: tmpdir, 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, "check for image")
assert.ErrorContains(t, err, "something went wrong") assert.ErrorContains(t, err, "something went wrong")
} }
@ -174,7 +174,7 @@ func TestDoUpdateNoVersion(t *testing.T) {
} }
client := baseClient{} 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") 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, "check for image")
assert.ErrorContains(t, err, "something went wrong") 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, "unable to pull")
assert.ErrorContains(t, err, "pull failure") assert.ErrorContains(t, err, "pull failure")
} }
@ -280,7 +280,7 @@ func TestActivateDoUpdateVerifyImageName(t *testing.T) {
RuntimeMetadataDir: tmpdir, 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, "check for image")
assert.ErrorContains(t, err, "something went wrong") assert.ErrorContains(t, err, "something went wrong")
expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion) expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)

View File

@ -13,6 +13,7 @@ import (
"github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/context/store"
manifeststore "github.com/docker/cli/cli/manifest/store" manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client" registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
clitypes "github.com/docker/cli/types" clitypes "github.com/docker/cli/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
@ -29,10 +30,10 @@ type FakeCli struct {
command.DockerCli command.DockerCli
client client.APIClient client client.APIClient
configfile *configfile.ConfigFile configfile *configfile.ConfigFile
out *command.OutStream out *streams.Out
outBuffer *bytes.Buffer outBuffer *bytes.Buffer
err *bytes.Buffer err *bytes.Buffer
in *command.InStream in *streams.In
server command.ServerInfo server command.ServerInfo
clientInfoFunc clientInfoFuncType clientInfoFunc clientInfoFuncType
notaryClientFunc NotaryClientFuncType notaryClientFunc NotaryClientFuncType
@ -51,10 +52,10 @@ func NewFakeCli(client client.APIClient, opts ...func(*FakeCli)) *FakeCli {
errBuffer := new(bytes.Buffer) errBuffer := new(bytes.Buffer)
c := &FakeCli{ c := &FakeCli{
client: client, client: client,
out: command.NewOutStream(outBuffer), out: streams.NewOut(outBuffer),
outBuffer: outBuffer, outBuffer: outBuffer,
err: errBuffer, 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 // 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.
configfile: configfile.New(""), 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 // 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 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 // 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 c.out = out
} }
@ -106,7 +107,7 @@ func (c *FakeCli) Client() client.APIClient {
} }
// Out returns the output stream (stdout) the cli should write on // 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 return c.out
} }
@ -116,7 +117,7 @@ func (c *FakeCli) Err() io.Writer {
} }
// In returns the input stream the cli will use // In returns the input stream the cli will use
func (c *FakeCli) In() *command.InStream { func (c *FakeCli) In() *streams.In {
return c.in return c.in
} }

View File

@ -11,7 +11,6 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/commands" "github.com/docker/cli/cli/command/commands"
"github.com/docker/docker/pkg/term"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/cobra/doc" "github.com/spf13/cobra/doc"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -37,8 +36,10 @@ func generateManPages(opts *options) error {
header.Date = &now header.Date = &now
} }
stdin, stdout, stderr := term.StdStreams() dockerCli, err := command.NewDockerCli()
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil) if err != nil {
return err
}
cmd := &cobra.Command{Use: "docker"} cmd := &cobra.Command{Use: "docker"}
commands.AddCommands(cmd, dockerCli) commands.AddCommands(cmd, dockerCli)
source := filepath.Join(opts.source, descriptionSourcePath) source := filepath.Join(opts.source, descriptionSourcePath)