Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 50 additions & 13 deletions cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}

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

Expand Down Expand Up @@ -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 {
Expand All @@ -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{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that some ops are always needed (e.g. to provide a stream). Would we want to stub those out to some default (either os.Std* or io.Discard I suppose)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added default operations:

func NewDockerCli(ops ...DockerCliOperator) (*DockerCli, error) {
	cli := &DockerCli{}
	defaultOps := []DockerCliOperator{
		WithStandardStreams(),
		WithContentTrustFromEnv(),
		WithContainerizedClient(containerizedengine.NewClient),
	}
	ops = append(defaultOps, ops...)
	if err := cli.Apply(ops...); err != nil {
		return nil, err
	}
	return cli, nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can override them as users operations are applied after the default ones.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current approach looks valid; there may other caveats (what if want without containerised client, or without content-trust)"

possible alternatives are;

  • only apply defaults if ops is empty
  • add a WithDefaults() option (that's applying these), which can be used if someone wants the defaults plus some custom configurations.

But we can discuss those solutions (not sure which approach makes the most sense)

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) {
Expand Down
89 changes: 89 additions & 0 deletions cli/command/cli_options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
45 changes: 45 additions & 0 deletions cli/command/cli_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package command

import (
"bytes"
"context"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"runtime"
"testing"

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"
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be happier that this test was really checking that if it was asserting that the in/out streams had FD() != 0 before this apply.

Oh, except it can be 0 for In anyway, since that is the stdin fd too. Not sure how best to get around that -- perhaps by calling something (like runVersion?) to actually output something into the buf and check it went where you hoped it would?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If fixed that directly reading or writing to the streams and check with their underlying buffers. 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks.

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")
}
4 changes: 2 additions & 2 deletions cli/command/context/export-import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"path/filepath"
"testing"

"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"gotest.tools/assert"
)

Expand Down Expand Up @@ -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", "-"))
Expand Down
4 changes: 2 additions & 2 deletions cli/command/image/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"))
Expand Down
3 changes: 2 additions & 1 deletion cli/command/image/trust.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
50 changes: 0 additions & 50 deletions cli/command/out.go

This file was deleted.

Loading