Finally rid of capital Sirupsen??

This commit is contained in:
Travis Reeder
2017-09-18 23:33:47 -07:00
parent f335d34636
commit 3aecebdf48
1127 changed files with 41199 additions and 41383 deletions

12
vendor/github.com/docker/cli/Jenkinsfile generated vendored Normal file
View File

@@ -0,0 +1,12 @@
wrappedNode(label: 'linux && x86_64', cleanWorkspace: true) {
timeout(time: 60, unit: 'MINUTES') {
stage "Git Checkout"
checkout scm
stage "Run end-to-end test suite"
sh "docker version"
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
IMAGE_TAG=clie2e${BUILD_NUMBER} \
make -f docker.Makefile test-e2e"
}
}

View File

@@ -27,6 +27,7 @@
people = [
"aaronlehmann",
"albers",
"aluzzardi",
"anusha",
"cpuguy83",
@@ -84,6 +85,11 @@
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.albers]
Name = "Harald Albers"
Email = "github@albersweb.de"
GitHub = "albers"
[people.aluzzardi]
Name = "Andrea Luzzardi"
Email = "al@docker.com"

View File

@@ -10,13 +10,16 @@ _:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
clean: ## remove build artifacts
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
.PHONY: test-unit
test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: test
test: ## run go test
./scripts/test/unit $(shell go list ./... | grep -v '/vendor/')
test: test-unit ## run tests
.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -v '/vendor/')
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: lint
lint: ## run all the lint tools

87
vendor/github.com/docker/cli/TESTING.md generated vendored Normal file
View File

@@ -0,0 +1,87 @@
# Testing
The following guidelines summarize the testing policy for docker/cli.
## Unit Test Suite
All code changes should have unit test coverage.
Error cases should be tested with unit tests.
Bug fixes should be covered by new unit tests or additional assertions in
existing unit tests.
### Details
The unit test suite follows the standard Go testing convention. Tests are
located in the package directory in `_test.go` files.
Unit tests should be named using the convention:
```
Test<Function Name><Test Case Name>
```
[Table tests](https://github.com/golang/go/wiki/TableDrivenTests) should be used
where appropriate, but may not be appropriate in all cases.
Assertions should be made using
[testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and test
requirements should be verified using
[testify/require](https://godoc.org/github.com/stretchr/testify/require).
Fakes, and testing utilities can be found in
[internal/test](https://godoc.org/github.com/docker/cli/internal/test) and
[gotestyourself](https://godoc.org/github.com/gotestyourself/gotestyourself).
## End-to-End Test Suite
The end-to-end test suite tests a cli binary against a real API backend.
### Guidelines
Each feature (subcommand) should have a single end-to-end test for
the success case. The test should include all (or most) flags/options supported
by that feature.
In some rare cases a couple additional end-to-end tests may be written for a
sufficiently complex and critical feature (ex: `container run`, `service
create`, `service update`, and `docker build` may have ~3-5 cases each).
In some rare cases a sufficiently critical error paths may have a single
end-to-end test case.
In all other cases the behaviour should be covered by unit tests.
If a code change adds a new flag, that flag should be added to the existing
"success case" end-to-end test.
If a code change fixes a bug, that bug fix should be covered either by adding
assertions to the existing end-to-end test, or with one or more unit test.
### Details
The end-to-end test suite is located in
[./e2e](https://github.com/docker/cli/tree/master/e2e). Each directory in `e2e`
corresponds to a directory in `cli/command` and contains the tests for that
subcommand. Files in each directory should be named `<command>_test.go` where
command is the basename of the command (ex: the test for `docker stack deploy`
is found in `e2e/stack/deploy_test.go`).
Tests should be named using the convention:
```
Test<Command Basename>[<Test Case Name>]
```
where the test case name is only required when there are multiple test cases for
a single command.
End-to-end test should run the `docker` binary using
[gotestyourself/icmd](https://godoc.org/github.com/gotestyourself/gotestyourself/icmd)
and make assertions about the exit code, stdout, stderr, and local file system.
Any Docker image or registry operations should use `registry:5000/<image name>`
to communicate with the local instance of the Docker registry. To load
additional fixture images to the registry see
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).

View File

@@ -1 +1 @@
17.08.0-dev
17.10.0-dev

View File

@@ -4,13 +4,13 @@ import (
"io"
"net/http/httputil"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/signal"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@@ -120,18 +120,7 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
}
if c.Config.Tty && dockerCli.Out().IsTerminal() {
height, width := dockerCli.Out().GetTtySize()
// To handle the case where a user repeatedly attaches/detaches without resizing their
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTtyTo(ctx, client, opts.container, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.
if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
resizeTTY(ctx, dockerCli, opts.container)
}
streamer := hijackedIOStreamer{
@@ -151,14 +140,36 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
if errAttach != nil {
return errAttach
}
return getExitStatus(ctx, dockerCli.Client(), opts.container)
}
_, status, err := getExitCode(ctx, dockerCli, opts.container)
if err != nil {
return err
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
height, width := dockerCli.Out().GetTtySize()
// To handle the case where a user repeatedly attaches/detaches without resizing their
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
// resize it, then go back to normal. Without this, every attach after the first will
// require the user to manually resize or hit enter.
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
// to the actual size.
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
logrus.Debugf("Error monitoring TTY size: %s", err)
}
}
func getExitStatus(ctx context.Context, apiclient client.ContainerAPIClient, containerID string) error {
container, err := apiclient.ContainerInspect(ctx, containerID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !client.IsErrConnectionFailed(err) {
return err
}
return cli.StatusError{StatusCode: -1}
}
status := container.State.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}

View File

@@ -4,10 +4,13 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNewAttachCommandErrors(t *testing.T) {
@@ -67,9 +70,48 @@ func TestNewAttachCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc}))
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc}))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestGetExitStatus(t *testing.T) {
containerID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
for _, testcase := range testcases {
client := &fakeClient{
inspectFunc: func(id string) (types.ContainerJSON, error) {
assert.Equal(t, containerID, id)
return types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{
State: &types.ContainerState{ExitCode: testcase.exitCode},
},
}, testcase.inspectError
},
}
err := getExitStatus(context.Background(), client, containerID)
assert.Equal(t, testcase.expectedError, err)
}
}

View File

@@ -1,19 +1,73 @@
package container
import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
containerInspectFunc func(string) (types.ContainerJSON, error)
inspectFunc func(string) (types.ContainerJSON, error)
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
infoFunc func() (types.Info, error)
}
func (cli *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if cli.containerInspectFunc != nil {
return cli.containerInspectFunc(containerID)
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if f.inspectFunc != nil {
return f.inspectFunc(containerID)
}
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(container, config)
}
return types.IDResponse{}, nil
}
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return types.ContainerExecInspect{}, nil
}
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return nil
}
func (f *fakeClient) ContainerCreate(
_ context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(config, hostConfig, networkingConfig, containerName)
}
return container.ContainerCreateCreatedBody{}, nil
}
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
return nil, nil
}
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return types.Info{}, nil
}

View File

@@ -113,6 +113,9 @@ type cidFile struct {
}
func (cid *cidFile) Close() error {
if cid.file == nil {
return nil
}
cid.file.Close()
if cid.written {
@@ -126,6 +129,9 @@ func (cid *cidFile) Close() error {
}
func (cid *cidFile) Write(id string) error {
if cid.file == nil {
return nil
}
if _, err := cid.file.Write([]byte(id)); err != nil {
return errors.Errorf("Failed to write the container ID to the file: %s", err)
}
@@ -134,6 +140,9 @@ func (cid *cidFile) Write(id string) error {
}
func newCIDFile(path string) (*cidFile, error) {
if path == "" {
return &cidFile{}, nil
}
if _, err := os.Stat(path); err == nil {
return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
}
@@ -153,19 +162,15 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
stderr := dockerCli.Err()
var (
containerIDFile *cidFile
trustedRef reference.Canonical
namedRef reference.Named
trustedRef reference.Canonical
namedRef reference.Named
)
cidfile := hostConfig.ContainerIDFile
if cidfile != "" {
var err error
if containerIDFile, err = newCIDFile(cidfile); err != nil {
return nil, err
}
defer containerIDFile.Close()
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
if err != nil {
return nil, err
}
defer containerIDFile.Close()
ref, err := reference.ParseAnyReference(config.Image)
if err != nil {
@@ -193,7 +198,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
@@ -215,10 +220,6 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
for _, warning := range response.Warnings {
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
}
if containerIDFile != nil {
if err = containerIDFile.Write(response.ID); err != nil {
return nil, err
}
}
return &response, nil
err = containerIDFile.Write(response.ID)
return &response, err
}

View File

@@ -0,0 +1,120 @@
package container
import (
"context"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCIDFileNoOPWithNoFilename(t *testing.T) {
file, err := newCIDFile("")
require.NoError(t, err)
assert.Equal(t, &cidFile{}, file)
assert.NoError(t, file.Write("id"))
assert.NoError(t, file.Close())
}
func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
tempfile := fs.NewFile(t, "test-cid-file")
defer tempfile.Remove()
_, err := newCIDFile(tempfile.Path())
testutil.ErrorContains(t, err, "Container ID file found")
}
func TestCIDFileCloseWithNoWrite(t *testing.T) {
tempdir := fs.NewDir(t, "test-cid-file")
defer tempdir.Remove()
path := tempdir.Join("cidfile")
file, err := newCIDFile(path)
require.NoError(t, err)
assert.Equal(t, file.path, path)
assert.NoError(t, file.Close())
_, err = os.Stat(path)
assert.True(t, os.IsNotExist(err))
}
func TestCIDFileCloseWithWrite(t *testing.T) {
tempdir := fs.NewDir(t, "test-cid-file")
defer tempdir.Remove()
path := tempdir.Join("cidfile")
file, err := newCIDFile(path)
require.NoError(t, err)
content := "id"
assert.NoError(t, file.Write(content))
actual, err := ioutil.ReadFile(path)
require.NoError(t, err)
assert.Equal(t, content, string(actual))
assert.NoError(t, file.Close())
_, err = os.Stat(path)
require.NoError(t, err)
}
func TestCreateContainerPullsImageIfMissing(t *testing.T) {
imageName := "does-not-exist-locally"
responseCounter := 0
containerID := "abcdef"
client := &fakeClient{
createContainerFunc: func(
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
defer func() { responseCounter++ }()
switch responseCounter {
case 0:
return container.ContainerCreateCreatedBody{}, fakeNotFound{}
case 1:
return container.ContainerCreateCreatedBody{ID: containerID}, nil
default:
return container.ContainerCreateCreatedBody{}, errors.New("unexpected")
}
},
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
infoFunc: func() (types.Info, error) {
return types.Info{IndexServerAddress: "http://indexserver"}, nil
},
}
cli := test.NewFakeCli(client)
config := &containerConfig{
Config: &container.Config{
Image: imageName,
},
HostConfig: &container.HostConfig{},
}
body, err := createContainer(context.Background(), cli, config, "name")
require.NoError(t, err)
expected := container.ContainerCreateCreatedBody{ID: containerID}
assert.Equal(t, expected, *body)
stderr := cli.ErrBuffer().String()
assert.Contains(t, stderr, "Unable to find image 'does-not-exist-locally:latest' locally")
}
type fakeNotFound struct{}
func (f fakeNotFound) NotFound() bool { return true }
func (f fakeNotFound) Error() string { return "error fake not found" }

View File

@@ -4,13 +4,15 @@ import (
"fmt"
"io"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
apiclient "github.com/docker/docker/client"
"github.com/docker/docker/pkg/promise"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@@ -22,14 +24,13 @@ type execOptions struct {
detach bool
user string
privileged bool
env *opts.ListOpts
env opts.ListOpts
container string
command []string
}
func newExecOptions() *execOptions {
var values []string
return &execOptions{
env: opts.NewListOptsRef(&values, opts.ValidateEnv),
}
func newExecOptions() execOptions {
return execOptions{env: opts.NewListOpts(opts.ValidateEnv)}
}
// NewExecCommand creates a new cobra.Command for `docker exec`
@@ -41,9 +42,9 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
Short: "Run a command in a running container",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
container := args[0]
execCmd := args[1:]
return runExec(dockerCli, options, container, execCmd)
options.container = args[0]
options.command = args[1:]
return runExec(dockerCli, options)
},
}
@@ -56,27 +57,14 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background")
flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
flags.VarP(options.env, "env", "e", "Set environment variables")
flags.VarP(&options.env, "env", "e", "Set environment variables")
flags.SetAnnotation("env", "version", []string{"1.25"})
return cmd
}
// nolint: gocyclo
func runExec(dockerCli command.Cli, options *execOptions, container string, execCmd []string) error {
execConfig, err := parseExec(options, execCmd)
// just in case the ParseExec does not exit
if container == "" || err != nil {
return cli.StatusError{StatusCode: 1}
}
if options.detachKeys != "" {
dockerCli.ConfigFile().DetachKeys = options.detachKeys
}
// Send client escape keys
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
func runExec(dockerCli command.Cli, options execOptions) error {
execConfig := parseExec(options, dockerCli.ConfigFile())
ctx := context.Background()
client := dockerCli.Client()
@@ -84,7 +72,7 @@ func runExec(dockerCli command.Cli, options *execOptions, container string, exec
// otherwise if we error out we will leak execIDs on the server (and
// there's no easy way to clean those up). But also in order to make "not
// exist" errors take precedence we do a dummy inspect first.
if _, err := client.ContainerInspect(ctx, container); err != nil {
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
return err
}
if !execConfig.Detach {
@@ -93,27 +81,27 @@ func runExec(dockerCli command.Cli, options *execOptions, container string, exec
}
}
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
if err != nil {
return err
}
execID := response.ID
if execID == "" {
fmt.Fprintln(dockerCli.Out(), "exec ID empty")
return nil
return errors.New("exec ID empty")
}
// Temp struct for execStart so that we don't need to transfer all the execConfig.
if execConfig.Detach {
execStartCheck := types.ExecStartCheck{
Detach: execConfig.Detach,
Tty: execConfig.Tty,
}
return client.ContainerExecStart(ctx, execID, execStartCheck)
}
return interactiveExec(ctx, dockerCli, execConfig, execID)
}
func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
// Interactive exec requested.
var (
out, stderr io.Writer
@@ -135,6 +123,7 @@ func runExec(dockerCli command.Cli, options *execOptions, container string, exec
}
}
client := dockerCli.Client()
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
if err != nil {
return err
@@ -165,42 +154,35 @@ func runExec(dockerCli command.Cli, options *execOptions, container string, exec
return err
}
var status int
if _, status, err = getExecExitCode(ctx, client, execID); err != nil {
return err
}
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
return getExecExitStatus(ctx, client, execID)
}
// getExecExitCode perform an inspect on the exec command. It returns
// the running state and the exit code.
func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) {
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
resp, err := client.ContainerExecInspect(ctx, execID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !apiclient.IsErrConnectionFailed(err) {
return false, -1, err
return err
}
return false, -1, nil
return cli.StatusError{StatusCode: -1}
}
return resp.Running, resp.ExitCode, nil
status := resp.ExitCode
if status != 0 {
return cli.StatusError{StatusCode: status}
}
return nil
}
// parseExec parses the specified args for the specified command and generates
// an ExecConfig from it.
func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) {
func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecConfig {
execConfig := &types.ExecConfig{
User: opts.user,
Privileged: opts.privileged,
Tty: opts.tty,
Cmd: execCmd,
Cmd: opts.command,
Detach: opts.detach,
Env: opts.env.GetAll(),
}
// If -d is not set, attach to everything by default
@@ -212,9 +194,10 @@ func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) {
}
}
if opts.env != nil {
execConfig.Env = opts.env.GetAll()
if opts.detachKeys != "" {
execConfig.DetachKeys = opts.detachKeys
} else {
execConfig.DetachKeys = configFile.DetachKeys
}
return execConfig, nil
return execConfig
}

View File

@@ -4,118 +4,201 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
type arguments struct {
options execOptions
execCmd []string
func withDefaultOpts(options execOptions) execOptions {
options.env = opts.NewListOpts(opts.ValidateEnv)
if len(options.command) == 0 {
options.command = []string{"command"}
}
return options
}
func TestParseExec(t *testing.T) {
valids := map[*arguments]*types.ExecConfig{
testcases := []struct {
options execOptions
configFile configfile.ConfigFile
expected types.ExecConfig
}{
{
execCmd: []string{"command"},
}: {
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
expected: types.ExecConfig{
Cmd: []string{"command"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(execOptions{}),
},
{
execCmd: []string{"command1", "command2"},
}: {
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
expected: types.ExecConfig{
Cmd: []string{"command1", "command2"},
AttachStdout: true,
AttachStderr: true,
},
options: withDefaultOpts(execOptions{
command: []string{"command1", "command2"},
}),
},
{
options: execOptions{
options: withDefaultOpts(execOptions{
interactive: true,
tty: true,
user: "uid",
}),
expected: types.ExecConfig{
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: []string{"command"},
},
execCmd: []string{"command"},
}: {
User: "uid",
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
Tty: true,
Cmd: []string{"command"},
},
{
options: execOptions{
detach: true,
options: withDefaultOpts(execOptions{detach: true}),
expected: types.ExecConfig{
Detach: true,
Cmd: []string{"command"},
},
execCmd: []string{"command"},
}: {
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Detach: true,
Cmd: []string{"command"},
},
{
options: execOptions{
options: withDefaultOpts(execOptions{
tty: true,
interactive: true,
detach: true,
}),
expected: types.ExecConfig{
Detach: true,
Tty: true,
Cmd: []string{"command"},
},
},
{
options: withDefaultOpts(execOptions{detach: true}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{
Cmd: []string{"command"},
DetachKeys: "de",
Detach: true,
},
},
{
options: withDefaultOpts(execOptions{
detach: true,
detachKeys: "ab",
}),
configFile: configfile.ConfigFile{DetachKeys: "de"},
expected: types.ExecConfig{
Cmd: []string{"command"},
DetachKeys: "ab",
Detach: true,
},
execCmd: []string{"command"},
}: {
AttachStdin: false,
AttachStdout: false,
AttachStderr: false,
Detach: true,
Tty: true,
Cmd: []string{"command"},
},
}
for valid, expectedExecConfig := range valids {
execConfig, err := parseExec(&valid.options, valid.execCmd)
require.NoError(t, err)
if !compareExecConfig(expectedExecConfig, execConfig) {
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
}
for _, testcase := range testcases {
execConfig := parseExec(testcase.options, &testcase.configFile)
assert.Equal(t, testcase.expected, *execConfig)
}
}
func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
if config1.AttachStderr != config2.AttachStderr {
return false
func TestRunExec(t *testing.T) {
var testcases = []struct {
doc string
options execOptions
client fakeClient
expectedError string
expectedOut string
expectedErr string
}{
{
doc: "successful detach",
options: withDefaultOpts(execOptions{
container: "thecontainer",
detach: true,
}),
client: fakeClient{execCreateFunc: execCreateWithID},
},
{
doc: "inspect error",
options: newExecOptions(),
client: fakeClient{
inspectFunc: func(string) (types.ContainerJSON, error) {
return types.ContainerJSON{}, errors.New("failed inspect")
},
},
expectedError: "failed inspect",
},
{
doc: "missing exec ID",
options: newExecOptions(),
expectedError: "exec ID empty",
},
}
if config1.AttachStdin != config2.AttachStdin {
return false
for _, testcase := range testcases {
t.Run(testcase.doc, func(t *testing.T) {
cli := test.NewFakeCli(&testcase.client)
err := runExec(cli, testcase.options)
if testcase.expectedError != "" {
testutil.ErrorContains(t, err, testcase.expectedError)
} else {
if !assert.NoError(t, err) {
return
}
}
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
})
}
if config1.AttachStdout != config2.AttachStdout {
return false
}
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
return types.IDResponse{ID: "execid"}, nil
}
func TestGetExecExitStatus(t *testing.T) {
execID := "the exec id"
expecatedErr := errors.New("unexpected error")
testcases := []struct {
inspectError error
exitCode int
expectedError error
}{
{
inspectError: nil,
exitCode: 0,
},
{
inspectError: expecatedErr,
expectedError: expecatedErr,
},
{
exitCode: 15,
expectedError: cli.StatusError{StatusCode: 15},
},
}
if config1.Detach != config2.Detach {
return false
}
if config1.Privileged != config2.Privileged {
return false
}
if config1.Tty != config2.Tty {
return false
}
if config1.User != config2.User {
return false
}
if len(config1.Cmd) != len(config2.Cmd) {
return false
}
for index, value := range config1.Cmd {
if value != config2.Cmd[index] {
return false
for _, testcase := range testcases {
client := &fakeClient{
execInspectFunc: func(id string) (types.ContainerExecInspect, error) {
assert.Equal(t, execID, id)
return types.ContainerExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
},
}
err := getExecExitStatus(context.Background(), client, execID)
assert.Equal(t, testcase.expectedError, err)
}
return true
}
func TestNewExecCommandErrors(t *testing.T) {
@@ -135,7 +218,7 @@ func TestNewExecCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc})
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
cmd := NewExecCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)

View File

@@ -6,12 +6,12 @@ import (
"runtime"
"sync"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)

View File

@@ -11,7 +11,6 @@ import (
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/container"
@@ -20,6 +19,7 @@ import (
"github.com/docker/docker/pkg/signal"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
)
@@ -274,7 +274,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
// Low-level execution (cgroups, namespaces, ...)
flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC mode to use")
flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology")
flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use")
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
@@ -421,11 +421,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
return nil, err
}
ipcMode := container.IpcMode(copts.ipcMode)
if !ipcMode.Valid() {
return nil, errors.Errorf("--ipc: invalid IPC mode")
}
pidMode := container.PidMode(copts.pidMode)
if !pidMode.Valid() {
return nil, errors.Errorf("--pid: invalid PID mode")
@@ -584,7 +579,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
ExtraHosts: copts.extraHosts.GetAll(),
VolumesFrom: copts.volumesFrom.GetAll(),
NetworkMode: container.NetworkMode(copts.netMode),
IpcMode: ipcMode,
IpcMode: container.IpcMode(copts.ipcMode),
PidMode: pidMode,
UTSMode: utsMode,
UsernsMode: usernsMode,

View File

@@ -1,8 +1,6 @@
package container
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
@@ -14,7 +12,6 @@ import (
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types/container"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/runconfig"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
@@ -115,7 +112,7 @@ func TestParseRunWithInvalidArgs(t *testing.T) {
}
// nolint: gocyclo
func TestParseRunVolumes(t *testing.T) {
func TestParseWithVolumes(t *testing.T) {
// A single volume
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
@@ -366,23 +363,12 @@ func TestParseDevice(t *testing.T) {
}
func TestParseModes(t *testing.T) {
// ipc ko
_, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"})
testutil.ErrorContains(t, err, "--ipc: invalid IPC mode")
// ipc ok
_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
require.NoError(t, err)
if !hostconfig.IpcMode.Valid() {
t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
}
// pid ko
_, _, _, err = parseRun([]string{"--pid=container:", "img", "cmd"})
_, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"})
testutil.ErrorContains(t, err, "--pid: invalid PID mode")
// pid ok
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
_, hostconfig, _, err := parseRun([]string{"--pid=host", "img", "cmd"})
require.NoError(t, err)
if !hostconfig.PidMode.Valid() {
t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
@@ -595,161 +581,6 @@ func TestParseEntryPoint(t *testing.T) {
}
}
// This tests the cases for binds which are generated through
// DecodeContainerConfig rather than Parse()
// nolint: gocyclo
func TestDecodeContainerConfigVolumes(t *testing.T) {
// Root to root
bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// No destination path
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// // No destination path or mode
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing
bindsOrVols = []string{`:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A whole lot of nothing with no mode
bindsOrVols = []string{`::`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Too much including an invalid mode
wTmp := os.Getenv("TEMP")
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Windows specific error tests
if runtime.GOOS == "windows" {
// Volume which does not include a drive letter
bindsOrVols = []string{`\tmp`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Root to C-Drive
bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// Container path that does not include a drive letter
bindsOrVols = []string{`c:\windows:\somewhere`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
}
// Linux-specific error tests
if runtime.GOOS != "windows" {
// Just root
bindsOrVols = []string{`/`}
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
t.Fatalf("binds %v should have failed", bindsOrVols)
}
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
t.Fatalf("volume %v should have failed", bindsOrVols)
}
// A single volume that looks like a bind mount passed in Volumes.
// This should be handled as a bind mount, not a volume.
vols := []string{`/foo:/bar`}
if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
} else if hostConfig.Binds != nil {
t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
} else if _, exists := config.Volumes[vols[0]]; !exists {
t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
}
}
}
// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes
// to call DecodeContainerConfig. It effectively does what a client would
// do when calling the daemon by constructing a JSON stream of a
// ContainerConfigWrapper which is populated by the set of volume specs
// passed into it. It returns a config and a hostconfig which can be
// validated to ensure DecodeContainerConfig has manipulated the structures
// correctly.
func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) {
var (
b []byte
err error
c *container.Config
h *container.HostConfig
)
w := runconfig.ContainerConfigWrapper{
Config: &container.Config{
Volumes: map[string]struct{}{},
},
HostConfig: &container.HostConfig{
NetworkMode: "none",
Binds: binds,
},
}
for _, v := range volumes {
w.Config.Volumes[v] = struct{}{}
}
if b, err = json.Marshal(w); err != nil {
return nil, nil, errors.Errorf("Error on marshal %s", err.Error())
}
c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b))
if err != nil {
return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err)
}
if c == nil || h == nil {
return nil, nil, errors.Errorf("Empty config or hostconfig")
}
return c, h, err
}
func TestValidateDevice(t *testing.T) {
valid := []string{
"/home",

View File

@@ -10,7 +10,6 @@ import (
"strings"
"syscall"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
@@ -20,6 +19,7 @@ import (
"github.com/docker/docker/pkg/signal"
"github.com/docker/docker/pkg/term"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"golang.org/x/net/context"

View File

@@ -7,11 +7,11 @@ import (
"sync"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)

View File

@@ -7,11 +7,11 @@ import (
"runtime"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/signal"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)

View File

@@ -3,14 +3,13 @@ package container
import (
"strconv"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/versions"
clientapi "github.com/docker/docker/client"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@@ -125,20 +124,6 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli,
return statusChan
}
// getExitCode performs an inspect on the container. It returns
// the running state and the exit code.
func getExitCode(ctx context.Context, dockerCli command.Cli, containerID string) (bool, int, error) {
c, err := dockerCli.Client().ContainerInspect(ctx, containerID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !clientapi.IsErrConnectionFailed(err) {
return false, -1, err
}
return false, -1, nil
}
return c.State.Running, c.State.ExitCode, nil
}
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil

View File

@@ -3,8 +3,8 @@ package command
import (
"sync"
"github.com/Sirupsen/logrus"
eventtypes "github.com/docker/docker/api/types/events"
"github.com/sirupsen/logrus"
)
// EventHandler is abstract interface for user to customize

View File

@@ -10,6 +10,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -230,10 +231,7 @@ size: 0B
// Special headers for customized table format
{
Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)},
`CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
`,
string(golden.Get(t, "container-context-write-special-headers.golden")),
},
}

View File

@@ -1,9 +1,10 @@
package formatter
import (
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func compareMultipleValues(t *testing.T, value, expected string) {
@@ -22,7 +23,5 @@ func compareMultipleValues(t *testing.T, value, expected string) {
keyval := strings.Split(expected, "=")
expMap[keyval[0]] = keyval[1]
}
if !reflect.DeepEqual(expMap, entriesMap) {
t.Fatalf("Expected entries: %v, got: %v", expected, value)
}
assert.Equal(t, expMap, entriesMap)
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"testing"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@@ -83,12 +84,7 @@ Build Cache 0B
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
},
},
`TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
Build Cache
`,
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
},
// Raw Format
{
@@ -97,31 +93,7 @@ Build Cache
Format: NewDiskUsageFormat("raw"),
},
},
`type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Build Cache
total:
active:
size: 0B
reclaimable: 0B
`,
string(golden.Get(t, "disk-usage-raw-format.golden")),
},
}

View File

@@ -86,9 +86,9 @@ func needDigest(ctx ImageContext) bool {
func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
for _, image := range images {
images := []*imageContext{}
formatted := []*imageContext{}
if isDangling(image) {
images = append(images, &imageContext{
formatted = append(formatted, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: "<none>",
@@ -96,90 +96,9 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
digest: "<none>",
})
} else {
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !needDigest(ctx) {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: "<none>",
})
continue
}
// Display the digests for each tag
for _, dgst := range digests {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: dgst,
})
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: "<none>",
digest: dgst,
})
}
} else {
images = append(images, &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: "<none>",
})
}
}
formatted = imageFormatTaggedAndDigest(ctx, image)
}
for _, imageCtx := range images {
for _, imageCtx := range formatted {
if err := format(imageCtx); err != nil {
return err
}
@@ -188,6 +107,82 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
return nil
}
func imageFormatTaggedAndDigest(ctx ImageContext, image types.ImageSummary) []*imageContext {
repoTags := map[string][]string{}
repoDigests := map[string][]string{}
images := []*imageContext{}
for _, refString := range image.RepoTags {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if nt, ok := ref.(reference.NamedTagged); ok {
familiarRef := reference.FamiliarName(ref)
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
}
}
for _, refString := range image.RepoDigests {
ref, err := reference.ParseNormalizedNamed(refString)
if err != nil {
continue
}
if c, ok := ref.(reference.Canonical); ok {
familiarRef := reference.FamiliarName(ref)
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
}
}
addImage := func(repo, tag, digest string) {
image := &imageContext{
trunc: ctx.Trunc,
i: image,
repo: repo,
tag: tag,
digest: digest,
}
images = append(images, image)
}
for repo, tags := range repoTags {
digests := repoDigests[repo]
// Do not display digests as their own row
delete(repoDigests, repo)
if !needDigest(ctx) {
// Ignore digest references, just show tag once
digests = nil
}
for _, tag := range tags {
if len(digests) == 0 {
addImage(repo, tag, "<none>")
continue
}
// Display the digests for each tag
for _, dgst := range digests {
addImage(repo, tag, dgst)
}
}
}
// Show rows for remaining digest only references
for repo, digests := range repoDigests {
// If digests are displayed, show row per digest
if ctx.Digest {
for _, dgst := range digests {
addImage(repo, "<none>", dgst)
}
} else {
addImage(repo, "<none>", "")
}
}
return images
}
type imageContext struct {
HeaderContext
trunc bool

View File

@@ -55,6 +55,26 @@ func TestImageContext(t *testing.T) {
i: types.ImageSummary{},
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest},
{
imageContext{
i: types.ImageSummary{Containers: 10},
}, "10", ctx.Containers,
},
{
imageContext{
i: types.ImageSummary{VirtualSize: 10000},
}, "10kB", ctx.VirtualSize,
},
{
imageContext{
i: types.ImageSummary{SharedSize: 10000},
}, "10kB", ctx.SharedSize,
},
{
imageContext{
i: types.ImageSummary{SharedSize: 5000, VirtualSize: 20000},
}, "15kB", ctx.UniqueSize,
},
}
for _, c := range cases {
@@ -62,8 +82,8 @@ func TestImageContext(t *testing.T) {
v := c.call()
if strings.Contains(v, ",") {
compareMultipleValues(t, v, c.expValue)
} else if v != c.expValue {
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
} else {
assert.Equal(t, c.expValue, v)
}
}
}

View File

@@ -8,6 +8,7 @@ import (
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@@ -120,10 +121,7 @@ func TestSearchContextWrite(t *testing.T) {
// Table format
{
Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]
`,
string(golden.Get(t, "search-context-write-table.golden")),
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},
@@ -210,9 +208,7 @@ func TestSearchContextWriteStars(t *testing.T) {
// Table format
{
Context{Format: NewSearchFormat("table")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
`,
string(golden.Get(t, "search-context-write-stars-table.golden")),
},
{
Context{Format: NewSearchFormat("table {{.Name}}")},

View File

@@ -12,19 +12,20 @@ import (
)
const (
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
secretIDHeader = "ID"
secretCreatedHeader = "CREATED"
secretUpdatedHeader = "UPDATED"
secretInspectPrettyTemplate Format = `ID: {{.ID}}
Name: {{.Name}}
secretInspectPrettyTemplate Format = `ID: {{.ID}}
Name: {{.Name}}
{{- if .Labels }}
Labels:
{{- range $k, $v := .Labels }}
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
{{- end }}{{ end }}
Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}`
Driver: {{.Driver}}
Created at: {{.CreatedAt}}
Updated at: {{.UpdatedAt}}`
)
// NewSecretFormat returns a Format for rendering using a secret Context
@@ -61,6 +62,7 @@ func newSecretContext() *secretContext {
sCtx.header = map[string]string{
"ID": secretIDHeader,
"Name": nameHeader,
"Driver": driverHeader,
"CreatedAt": secretCreatedHeader,
"UpdatedAt": secretUpdatedHeader,
"Labels": labelsHeader,
@@ -89,6 +91,13 @@ func (c *secretContext) CreatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago"
}
func (c *secretContext) Driver() string {
if c.s.Spec.Driver == nil {
return ""
}
return c.s.Spec.Driver.Name
}
func (c *secretContext) UpdatedAt() string {
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago"
}
@@ -153,6 +162,13 @@ func (ctx *secretInspectContext) Labels() map[string]string {
return ctx.Secret.Spec.Labels
}
func (ctx *secretInspectContext) Driver() string {
if ctx.Secret.Spec.Driver == nil {
return ""
}
return ctx.Secret.Spec.Driver.Name
}
func (ctx *secretInspectContext) CreatedAt() string {
return command.PrettyPrint(ctx.Secret.CreatedAt)
}

View File

@@ -28,9 +28,9 @@ func TestSecretContextFormatWrite(t *testing.T) {
},
// Table format
{Context{Format: NewSecretFormat("table", false)},
`ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`ID NAME DRIVER CREATED UPDATED
1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago
`},
{Context{Format: NewSecretFormat("table {{.Name}}", true)},
`NAME

View File

@@ -504,7 +504,10 @@ func (c *serviceContext) Replicas() string {
}
func (c *serviceContext) Image() string {
image := c.service.Spec.TaskTemplate.ContainerSpec.Image
var image string
if c.service.Spec.TaskTemplate.ContainerSpec != nil {
image = c.service.Spec.TaskTemplate.ContainerSpec.Image
}
if ref, err := reference.ParseNormalizedNamed(image); err == nil {
// update image string for display, (strips any digest)
if nt, ok := ref.(reference.NamedTagged); ok {

View File

@@ -8,6 +8,7 @@ import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -59,21 +60,7 @@ bar
// Raw Format
{
Context{Format: NewServiceListFormat("raw", false)},
`id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp
`,
string(golden.Get(t, "service-context-write-raw.golden")),
},
{
Context{Format: NewServiceListFormat("raw", true)},

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
@@ -33,10 +34,7 @@ taskID2
},
{
Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)},
`NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2
`,
string(golden.Get(t, "task-context-write-table-custom.golden")),
},
{
Context{Format: NewTaskFormat("table {{.Name}}", true)},

View File

@@ -0,0 +1,3 @@
CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
conta "ubuntu" 24 hours ago//.FOOBAR_BAR

View File

@@ -0,0 +1,5 @@
TYPE ACTIVE
Images 0
Containers 0
Local Volumes 0
Build Cache

View File

@@ -0,0 +1,24 @@
type: Images
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Containers
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Local Volumes
total: 0
active: 0
size: 0B
reclaimable: 0B
type: Build Cache
total:
active:
size: 0B
reclaimable: 0B

View File

@@ -0,0 +1,2 @@
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]

View File

@@ -0,0 +1,3 @@
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]

View File

@@ -0,0 +1,14 @@
id: id_baz
name: baz
mode: global
replicas: 2/4
image:
ports: *:80->8080/tcp
id: id_bar
name: bar
mode: replicated
replicas: 2/4
image:
ports: *:80->8080/tcp

View File

@@ -0,0 +1,3 @@
NAME NODE PORTS
foobar_baz foo1
foobar_bar foo2

View File

@@ -12,7 +12,6 @@ import (
"regexp"
"runtime"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
@@ -22,12 +21,14 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/urlutil"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@@ -243,6 +244,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
ExcludePatterns: excludes,
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
})
if err != nil {
return err

View File

@@ -17,9 +17,9 @@ import (
"github.com/docker/cli/cli/command/image/build"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client/session"
"github.com/docker/docker/client/session/filesync"
"github.com/docker/docker/pkg/progress"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/filesync"
"github.com/pkg/errors"
"golang.org/x/time/rate"
)

View File

@@ -6,18 +6,57 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"syscall"
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/skip"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
)
func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
skip.IfCondition(t, runtime.GOOS == "windows", "uid and gid not relevant on windows")
dest := fs.NewDir(t, "test-build-context-dest")
defer dest.Remove()
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
assert.NoError(t, archive.Untar(context, dest.Path(), nil))
body := new(bytes.Buffer)
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
}
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
dir := fs.NewDir(t, "test-build-context",
fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
fs.WithFile("Dockerfile", `
FROM alpine:3.6
COPY foo bar /
`),
)
defer dir.Remove()
options := newBuildOptions()
options.context = dir.Path()
err := runBuild(cli, options)
require.NoError(t, err)
files, err := ioutil.ReadDir(dest.Path())
require.NoError(t, err)
for _, fileInfo := range files {
assert.Equal(t, uint32(0), fileInfo.Sys().(*syscall.Stat_t).Uid)
assert.Equal(t, uint32(0), fileInfo.Sys().(*syscall.Stat_t).Gid)
}
}
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
dest, err := ioutil.TempDir("", "test-build-compress-dest")
require.NoError(t, err)

View File

@@ -8,7 +8,6 @@ import (
"path"
"sort"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
@@ -19,6 +18,7 @@ import (
"github.com/docker/notary/tuf/data"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)

View File

@@ -7,10 +7,10 @@ import (
"strings"
"text/template"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/templates"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Inspector defines an interface to implement to process elements

View File

@@ -12,6 +12,7 @@ type fakeClient struct {
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
}
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
@@ -34,3 +35,10 @@ func (c *fakeClient) NetworkDisconnect(ctx context.Context, networkID, container
}
return nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return []types.NetworkResource{}, nil
}

View File

@@ -0,0 +1,69 @@
package network
import (
"testing"
"io/ioutil"
"strings"
"github.com/docker/cli/internal/test"
. "github.com/docker/cli/internal/test/builders"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNetworkListErrors(t *testing.T) {
testCases := []struct {
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
expectedError string
}{
{
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
return []types.NetworkResource{}, errors.Errorf("error creating network")
},
expectedError: "error creating network",
},
}
for _, tc := range testCases {
cmd := newListCommand(
test.NewFakeCli(&fakeClient{
networkListFunc: tc.networkListFunc,
}),
)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestNetworkListWithFlags(t *testing.T) {
filterArgs := filters.NewArgs()
filterArgs.Add("image.name", "ubuntu")
expectedOpts := types.NetworkListOptions{
Filters: filterArgs,
}
cli := test.NewFakeCli(&fakeClient{
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
assert.Equal(t, expectedOpts, options, "not expected options error")
return []types.NetworkResource{*NetworkResource(NetworkResourceID("123454321"),
NetworkResourceName("network_1"),
NetworkResourceDriver("09.7.01"),
NetworkResourceScope("global"))}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("filter", "image.name=ubuntu")
assert.NoError(t, cmd.Execute())
golden.Assert(t, strings.TrimSpace(cli.OutBuffer().String()), "network-list.golden")
}

View File

@@ -0,0 +1,2 @@
NETWORK ID NAME DRIVER SCOPE
123454321 network_1 09.7.01 global

View File

@@ -4,8 +4,8 @@ import (
"io"
"os"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/term"
"github.com/sirupsen/logrus"
)
// OutStream is an output stream used by the DockerCli to write normal program

View File

@@ -7,13 +7,13 @@ import (
"os"
"path/filepath"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)

View File

@@ -17,6 +17,7 @@ import (
type createOptions struct {
name string
driver string
file string
labels opts.ListOpts
}
@@ -27,17 +28,21 @@ func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command {
}
cmd := &cobra.Command{
Use: "create [OPTIONS] SECRET file|-",
Use: "create [OPTIONS] SECRET [file|-]",
Short: "Create a secret from a file or STDIN as content",
Args: cli.ExactArgs(2),
Args: cli.RequiresRangeArgs(1, 2),
RunE: func(cmd *cobra.Command, args []string) error {
options.name = args[0]
options.file = args[1]
if len(args) == 2 {
options.file = args[1]
}
return runSecretCreate(dockerCli, options)
},
}
flags := cmd.Flags()
flags.VarP(&options.labels, "label", "l", "Secret labels")
flags.StringVarP(&options.driver, "driver", "d", "", "Secret driver")
flags.SetAnnotation("driver", "version", []string{"1.31"})
return cmd
}
@@ -46,21 +51,14 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
client := dockerCli.Client()
ctx := context.Background()
var in io.Reader = dockerCli.In()
if options.file != "-" {
file, err := system.OpenSequential(options.file)
if err != nil {
return err
}
in = file
defer file.Close()
if options.driver != "" && options.file != "" {
return errors.Errorf("When using secret driver secret data must be empty")
}
secretData, err := ioutil.ReadAll(in)
secretData, err := readSecretData(dockerCli.In(), options.file)
if err != nil {
return errors.Errorf("Error reading content from %q: %v", options.file, err)
}
spec := swarm.SecretSpec{
Annotations: swarm.Annotations{
Name: options.name,
@@ -68,6 +66,11 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
},
Data: secretData,
}
if options.driver != "" {
spec.Driver = &swarm.Driver{
Name: options.driver,
}
}
r, err := client.SecretCreate(ctx, spec)
if err != nil {
@@ -77,3 +80,23 @@ func runSecretCreate(dockerCli command.Cli, options createOptions) error {
fmt.Fprintln(dockerCli.Out(), r.ID)
return nil
}
func readSecretData(in io.ReadCloser, file string) ([]byte, error) {
// Read secret value from external driver
if file == "" {
return nil, nil
}
if file != "-" {
var err error
in, err = system.OpenSequential(file)
if err != nil {
return nil, err
}
defer in.Close()
}
data, err := ioutil.ReadAll(in)
if err != nil {
return nil, err
}
return data, nil
}

View File

@@ -24,12 +24,11 @@ func TestSecretCreateErrors(t *testing.T) {
secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error)
expectedError string
}{
{
args: []string{"too_few"},
expectedError: "requires exactly 2 arguments",
},
{args: []string{"too", "many", "arguments"},
expectedError: "requires exactly 2 arguments",
expectedError: "requires at least 1 and at most 2 arguments",
},
{args: []string{"create", "--driver", "driver", "-"},
expectedError: "secret data must be empty",
},
{
args: []string{"name", filepath.Join("testdata", secretDataFile)},
@@ -75,6 +74,35 @@ func TestSecretCreateWithName(t *testing.T) {
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
func TestSecretCreateWithDriver(t *testing.T) {
expectedDriver := &swarm.Driver{
Name: "secret-driver",
}
name := "foo"
cli := test.NewFakeCli(&fakeClient{
secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
if spec.Name != name {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
}
if !reflect.DeepEqual(spec.Driver.Name, expectedDriver.Name) {
return types.SecretCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
}
return types.SecretCreateResponse{
ID: "ID-" + spec.Name,
}, nil
},
})
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name})
cmd.Flags().Set("driver", expectedDriver.Name)
assert.NoError(t, cmd.Execute())
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}
func TestSecretCreateWithLabels(t *testing.T) {
expectedLabels := map[string]string{
"lbl1": "Label-foo",

View File

@@ -154,6 +154,7 @@ func TestSecretInspectPretty(t *testing.T) {
}),
SecretID("secretID"),
SecretName("secretName"),
SecretDriver("driver"),
SecretCreatedAt(time.Time{}),
SecretUpdatedAt(time.Time{}),
), []byte{}, nil

View File

@@ -63,6 +63,7 @@ func TestSecretList(t *testing.T) {
SecretVersion(swarm.Version{Index: 11}),
SecretCreatedAt(time.Now().Add(-2*time.Hour)),
SecretUpdatedAt(time.Now().Add(-1*time.Hour)),
SecretDriver("driver"),
),
}, nil
},

View File

@@ -1,6 +1,7 @@
ID: secretID
Name: secretName
ID: secretID
Name: secretName
Labels:
- lbl1=value1
Created at: 0001-01-01 00:00:00 +0000 utc
Updated at: 0001-01-01 00:00:00 +0000 utc
Driver: driver
Created at: 0001-01-01 00:00:00 +0000 utc
Updated at: 0001-01-01 00:00:00 +0000 utc

View File

@@ -1,3 +1,3 @@
ID NAME CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago
ID NAME DRIVER CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago

View File

@@ -1,3 +1,3 @@
ID NAME CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar 2 hours ago About an hour ago
ID NAME DRIVER CREATED UPDATED
ID-foo foo 2 hours ago About an hour ago
ID-bar bar driver 2 hours ago About an hour ago

View File

@@ -14,6 +14,7 @@ type fakeClient struct {
serviceInspectWithRawFunc func(ctx context.Context, serviceID string, options types.ServiceInspectOptions) (swarm.Service, []byte, error)
serviceUpdateFunc func(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error)
serviceListFunc func(context.Context, types.ServiceListOptions) ([]swarm.Service, error)
infoFunc func(ctx context.Context) (types.Info, error)
}
func (f *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) {
@@ -48,6 +49,13 @@ func (f *fakeClient) ServiceUpdate(ctx context.Context, serviceID string, versio
return types.ServiceUpdateResponse{}, nil
}
func (f *fakeClient) Info(ctx context.Context) (types.Info, error) {
if f.infoFunc == nil {
return types.Info{}, nil
}
return f.infoFunc(ctx)
}
func newService(id string, name string) swarm.Service {
return swarm.Service{
ID: id,

View File

@@ -123,8 +123,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *serviceOptions
fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID)
if opts.detach {
warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "created")
if opts.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
return nil
}

View File

@@ -1,15 +1,12 @@
package service
import (
"fmt"
"io"
"io/ioutil"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/service/progress"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/spf13/pflag"
"golang.org/x/net/context"
)
@@ -34,11 +31,3 @@ func waitOnService(ctx context.Context, dockerCli command.Cli, serviceID string,
}
return err
}
// warnDetachDefault warns about the --detach flag future change if it's supported.
func warnDetachDefault(err io.Writer, clientVersion string, flags *pflag.FlagSet, msg string) {
if !flags.Changed("detach") && versions.GreaterThanOrEqualTo(clientVersion, "1.29") {
fmt.Fprintf(err, "Since --detach=false was not specified, tasks will be %s in the background.\n"+
"In a future release, --detach=false will become the default.\n", msg)
}
}

View File

@@ -1,40 +0,0 @@
package service
import (
"bytes"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
)
func TestWarnDetachDefault(t *testing.T) {
var detach bool
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
addDetachFlag(flags, &detach)
var tests = []struct {
detach bool
version string
expectWarning bool
}{
{true, "1.28", false},
{true, "1.29", false},
{false, "1.28", false},
{false, "1.29", true},
}
for _, test := range tests {
out := new(bytes.Buffer)
flags.Lookup(flagDetach).Changed = test.detach
warnDetachDefault(out, test.version, flags, "")
if test.expectWarning {
assert.NotEmpty(t, out.String(), "expected warning")
} else {
assert.Empty(t, out.String(), "expected no warning")
}
}
}

View File

@@ -41,7 +41,7 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time)
Labels: map[string]string{"com.label": "foo"},
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: "foo/bar@sha256:this_is_a_test",
},
Networks: []swarm.NetworkAttachmentConfig{

View File

@@ -13,6 +13,7 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/cli/service/logs"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
@@ -257,7 +258,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) {
return 0, errors.Errorf("invalid context in log message: %v", string(buf))
}
// parse the details out
details, err := client.ParseLogDetails(string(parts[detailsIndex]))
details, err := logs.ParseLogDetails(string(parts[detailsIndex]))
if err != nil {
return 0, err
}

View File

@@ -592,7 +592,7 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: options.image,
Args: options.args,
Command: options.entrypoint.Value(),
@@ -691,7 +691,7 @@ func buildServiceDefaultFlagMapping() flagDefaults {
}
func addDetachFlag(flags *pflag.FlagSet, detach *bool) {
flags.BoolVarP(detach, flagDetach, "d", true, "Exit immediately instead of waiting for the service to converge")
flags.BoolVarP(detach, flagDetach, "d", false, "Exit immediately instead of waiting for the service to converge")
flags.SetAnnotation(flagDetach, "version", []string{"1.29"})
}

View File

@@ -56,6 +56,9 @@ func runPS(dockerCli command.Cli, options psOptions) error {
if err != nil {
return err
}
if err := updateNodeFilter(ctx, client, filter); err != nil {
return err
}
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter})
if err != nil {
@@ -130,16 +133,20 @@ loop:
if serviceCount == 0 {
return filter, nil, errors.New(strings.Join(notfound, "\n"))
}
return filter, notfound, err
}
func updateNodeFilter(ctx context.Context, client client.APIClient, filter filters.Args) error {
if filter.Include("node") {
nodeFilters := filter.Get("node")
for _, nodeFilter := range nodeFilters {
nodeReference, err := node.Reference(ctx, client, nodeFilter)
if err != nil {
return filter, nil, err
return err
}
filter.Del("node", nodeFilter)
filter.Add("node", nodeReference)
}
}
return filter, notfound, err
return nil
}

View File

@@ -89,3 +89,25 @@ func TestRunPSWarnsOnNotFound(t *testing.T) {
err := runPS(cli, options)
assert.EqualError(t, err, "no such service: bar")
}
func TestUpdateNodeFilter(t *testing.T) {
selfNodeID := "foofoo"
filter := filters.NewArgs()
filter.Add("node", "one")
filter.Add("node", "two")
filter.Add("node", "self")
client := &fakeClient{
infoFunc: func(_ context.Context) (types.Info, error) {
return types.Info{Swarm: swarm.Info{NodeID: selfNodeID}}, nil
},
}
updateNodeFilter(context.Background(), client, filter)
expected := filters.NewArgs()
expected.Add("node", "one")
expected.Add("node", "two")
expected.Add("node", selfNodeID)
assert.Equal(t, expected, filter)
}

View File

@@ -7,8 +7,8 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
@@ -19,7 +19,7 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
Short: "Revert changes to a service's configuration",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runRollback(dockerCli, cmd.Flags(), options, args[0])
return runRollback(dockerCli, options, args[0])
},
Tags: map[string]string{"version": "1.31"},
}
@@ -31,7 +31,7 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
func runRollback(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOptions, serviceID string) error {
func runRollback(dockerCli command.Cli, options *serviceOptions, serviceID string) error {
apiClient := dockerCli.Client()
ctx := context.Background()
@@ -56,8 +56,7 @@ func runRollback(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOp
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
if options.detach {
warnDetachDefault(dockerCli.Err(), apiClient.ClientVersion(), flags, "rolled back")
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
return nil
}

View File

@@ -10,9 +10,9 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/versions"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type scaleOptions struct {
@@ -27,7 +27,7 @@ func newScaleCommand(dockerCli command.Cli) *cobra.Command {
Short: "Scale one or multiple replicated services",
Args: scaleArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runScale(dockerCli, cmd.Flags(), options, args)
return runScale(dockerCli, options, args)
},
}
@@ -54,7 +54,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error {
return nil
}
func runScale(dockerCli command.Cli, flags *pflag.FlagSet, options *scaleOptions, args []string) error {
func runScale(dockerCli command.Cli, options *scaleOptions, args []string) error {
var errs []string
var serviceIDs []string
ctx := context.Background()
@@ -79,9 +79,7 @@ func runScale(dockerCli command.Cli, flags *pflag.FlagSet, options *scaleOptions
}
if len(serviceIDs) > 0 {
if options.detach {
warnDetachDefault(dockerCli.Err(), dockerCli.Client().ClientVersion(), flags, "scaled")
} else {
if !options.detach && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.29") {
for _, serviceID := range serviceIDs {
if err := waitOnService(ctx, dockerCli, serviceID, false); err != nil {
errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err))

View File

@@ -3,7 +3,6 @@ package service
import (
"encoding/hex"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
@@ -12,6 +11,7 @@ import (
"github.com/docker/notary/tuf/data"
"github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/net/context"
)

View File

@@ -216,8 +216,7 @@ func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, options *serviceOpti
fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID)
if options.detach {
warnDetachDefault(dockerCli.Err(), dockerCli.Client().ClientVersion(), flags, "updated")
if options.detach || versions.LessThan(apiClient.ClientVersion(), "1.29") {
return nil
}
@@ -270,7 +269,7 @@ func updateService(ctx context.Context, apiClient client.NetworkAPIClient, flags
}
}
cspec := &spec.TaskTemplate.ContainerSpec
cspec := spec.TaskTemplate.ContainerSpec
task := &spec.TaskTemplate
taskResources := func() *swarm.ResourceRequirements {

View File

@@ -19,8 +19,12 @@ func TestUpdateServiceArgs(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
flags.Set("args", "the \"new args\"")
spec := &swarm.ServiceSpec{}
cspec := &spec.TaskTemplate.ContainerSpec
spec := &swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
cspec := spec.TaskTemplate.ContainerSpec
cspec.Args = []string{"old", "args"}
updateService(nil, nil, flags, spec)
@@ -452,8 +456,12 @@ func TestUpdateSecretUpdateInPlace(t *testing.T) {
}
func TestUpdateReadOnly(t *testing.T) {
spec := &swarm.ServiceSpec{}
cspec := &spec.TaskTemplate.ContainerSpec
spec := &swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
cspec := spec.TaskTemplate.ContainerSpec
// Update with --read-only=true, changed to true
flags := newUpdateCommand(nil).Flags()
@@ -474,8 +482,12 @@ func TestUpdateReadOnly(t *testing.T) {
}
func TestUpdateStopSignal(t *testing.T) {
spec := &swarm.ServiceSpec{}
cspec := &spec.TaskTemplate.ContainerSpec
spec := &swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{},
},
}
cspec := spec.TaskTemplate.ContainerSpec
// Update with --stop-signal=SIGUSR1
flags := newUpdateCommand(nil).Flags()

View File

@@ -64,7 +64,7 @@ func deployBundle(ctx context.Context, dockerCli command.Cli, opts deployOptions
Labels: convert.AddStackLabel(namespace, service.Labels),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: service.Image,
Command: service.Command,
Args: service.Args,

View File

@@ -45,7 +45,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
Labels: map[string]string{"com.docker.stack.image": "foobar:1.2.3"},
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: "foobar:1.2.3@sha256:deadbeef",
},
},
@@ -86,7 +86,7 @@ func TestServiceUpdateResolveImageChanged(t *testing.T) {
spec := map[string]swarm.ServiceSpec{
"myservice": {
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: testcase.image,
},
},

View File

@@ -57,8 +57,7 @@ func runPS(dockerCli command.Cli, options psOptions) error {
}
if len(tasks) == 0 {
fmt.Fprintf(dockerCli.Err(), "Nothing found in stack: %s\n", namespace)
return nil
return fmt.Errorf("nothing found in stack: %s", namespace)
}
format := options.format

View File

@@ -60,9 +60,9 @@ func TestStackPsEmptyStack(t *testing.T) {
cmd := newPsCommand(fakeCli)
cmd.SetArgs([]string{"foo"})
assert.NoError(t, cmd.Execute())
assert.Error(t, cmd.Execute())
assert.EqualError(t, cmd.Execute(), "nothing found in stack: foo")
assert.Equal(t, "", fakeCli.OutBuffer().String())
assert.Equal(t, "Nothing found in stack: foo\n", fakeCli.ErrBuffer().String())
}
func TestStackPsWithQuietOption(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package stack
import (
"fmt"
"sort"
"strings"
"github.com/docker/cli/cli"
@@ -88,14 +89,21 @@ func runRemove(dockerCli command.Cli, opts removeOptions) error {
return nil
}
func sortServiceByName(services []swarm.Service) func(i, j int) bool {
return func(i, j int) bool {
return services[i].Spec.Name < services[j].Spec.Name
}
}
func removeServices(
ctx context.Context,
dockerCli command.Cli,
services []swarm.Service,
) bool {
var hasError bool
sort.Slice(services, sortServiceByName(services))
for _, service := range services {
fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name)
fmt.Fprintf(dockerCli.Out(), "Removing service %s\n", service.Spec.Name)
if err := dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err)
@@ -111,7 +119,7 @@ func removeNetworks(
) bool {
var hasError bool
for _, network := range networks {
fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name)
fmt.Fprintf(dockerCli.Out(), "Removing network %s\n", network.Name)
if err := dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err)
@@ -127,7 +135,7 @@ func removeSecrets(
) bool {
var hasError bool
for _, secret := range secrets {
fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name)
fmt.Fprintf(dockerCli.Out(), "Removing secret %s\n", secret.Spec.Name)
if err := dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err)
@@ -143,7 +151,7 @@ func removeConfigs(
) bool {
var hasError bool
for _, config := range configs {
fmt.Fprintf(dockerCli.Err(), "Removing config %s\n", config.Spec.Name)
fmt.Fprintf(dockerCli.Out(), "Removing config %s\n", config.Spec.Name)
if err := dockerCli.Client().ConfigRemove(ctx, config.ID); err != nil {
hasError = true
fmt.Fprintf(dockerCli.Err(), "Failed to remove config %s: %s", config.ID, err)

View File

@@ -101,7 +101,13 @@ func TestRemoveStackSkipEmpty(t *testing.T) {
cmd.SetArgs([]string{"foo", "bar"})
assert.NoError(t, cmd.Execute())
assert.Equal(t, "", fakeCli.OutBuffer().String())
expectedList := []string{"Removing service bar_service1",
"Removing service bar_service2",
"Removing secret bar_secret1",
"Removing config bar_config1",
"Removing network bar_network1\n",
}
assert.Equal(t, strings.Join(expectedList, "\n"), fakeCli.OutBuffer().String())
assert.Contains(t, fakeCli.ErrBuffer().String(), "Nothing found in stack: foo\n")
assert.Equal(t, allServiceIDs, fakeClient.removedServices)
assert.Equal(t, allNetworkIDs, fakeClient.removedNetworks)

View File

@@ -54,109 +54,51 @@ func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error {
}
// nolint: gocyclo
func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
fmt.Fprintf(dockerCli.Out(), "Containers: %d\n", info.Containers)
fmt.Fprintf(dockerCli.Out(), " Running: %d\n", info.ContainersRunning)
fmt.Fprintf(dockerCli.Out(), " Paused: %d\n", info.ContainersPaused)
fmt.Fprintf(dockerCli.Out(), " Stopped: %d\n", info.ContainersStopped)
fmt.Fprintf(dockerCli.Out(), "Images: %d\n", info.Images)
fprintfIfNotEmpty(dockerCli.Out(), "Server Version: %s\n", info.ServerVersion)
fprintfIfNotEmpty(dockerCli.Out(), "Storage Driver: %s\n", info.Driver)
func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
fmt.Fprintln(dockerCli.Out(), "Containers:", info.Containers)
fmt.Fprintln(dockerCli.Out(), " Running:", info.ContainersRunning)
fmt.Fprintln(dockerCli.Out(), " Paused:", info.ContainersPaused)
fmt.Fprintln(dockerCli.Out(), " Stopped:", info.ContainersStopped)
fmt.Fprintln(dockerCli.Out(), "Images:", info.Images)
fprintlnNonEmpty(dockerCli.Out(), "Server Version:", info.ServerVersion)
fprintlnNonEmpty(dockerCli.Out(), "Storage Driver:", info.Driver)
if info.DriverStatus != nil {
for _, pair := range info.DriverStatus {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1])
}
}
if info.SystemStatus != nil {
for _, pair := range info.SystemStatus {
fmt.Fprintf(dockerCli.Out(), "%s: %s\n", pair[0], pair[1])
}
}
fprintfIfNotEmpty(dockerCli.Out(), "Logging Driver: %s\n", info.LoggingDriver)
fprintfIfNotEmpty(dockerCli.Out(), "Cgroup Driver: %s\n", info.CgroupDriver)
fprintlnNonEmpty(dockerCli.Out(), "Logging Driver:", info.LoggingDriver)
fprintlnNonEmpty(dockerCli.Out(), "Cgroup Driver:", info.CgroupDriver)
fmt.Fprintf(dockerCli.Out(), "Plugins: \n")
fmt.Fprintf(dockerCli.Out(), " Volume:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Volume, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprintf(dockerCli.Out(), " Network:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Network, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprintln(dockerCli.Out(), "Plugins:")
fmt.Fprintln(dockerCli.Out(), " Volume:", strings.Join(info.Plugins.Volume, " "))
fmt.Fprintln(dockerCli.Out(), " Network:", strings.Join(info.Plugins.Network, " "))
if len(info.Plugins.Authorization) != 0 {
fmt.Fprintf(dockerCli.Out(), " Authorization:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Authorization, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprintln(dockerCli.Out(), " Authorization:", strings.Join(info.Plugins.Authorization, " "))
}
fmt.Fprintf(dockerCli.Out(), " Log:")
fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Log, " "))
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprintln(dockerCli.Out(), " Log:", strings.Join(info.Plugins.Log, " "))
fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState)
if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && info.Swarm.LocalNodeState != swarm.LocalNodeStateLocked {
fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID)
if info.Swarm.Error != "" {
fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error)
}
fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable)
if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError {
fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID)
fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers)
fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes)
fmt.Fprintf(dockerCli.Out(), " Orchestration:\n")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
}
fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", taskHistoryRetentionLimit)
fmt.Fprintf(dockerCli.Out(), " Raft:\n")
fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval)
if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil {
fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots)
}
fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick)
fmt.Fprintf(dockerCli.Out(), " Election Tick: %d\n", info.Swarm.Cluster.Spec.Raft.ElectionTick)
fmt.Fprintf(dockerCli.Out(), " Dispatcher:\n")
fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))
fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n")
fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
fmt.Fprintf(dockerCli.Out(), " Force Rotate: %d\n", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
fprintfIfNotEmpty(dockerCli.Out(), " Signing CA Certificate: \n%s\n\n", strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert))
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
fmt.Fprintf(dockerCli.Out(), " External CAs:\n")
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL)
}
}
fmt.Fprintf(dockerCli.Out(), " Root Rotation In Progress: %v\n", info.Swarm.Cluster.RootRotationInProgress)
}
fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr)
managers := []string{}
for _, entry := range info.Swarm.RemoteManagers {
managers = append(managers, entry.Addr)
}
if len(managers) > 0 {
sort.Strings(managers)
fmt.Fprintf(dockerCli.Out(), " Manager Addresses:\n")
for _, entry := range managers {
fmt.Fprintf(dockerCli.Out(), " %s\n", entry)
}
}
}
fmt.Fprintln(dockerCli.Out(), "Swarm:", info.Swarm.LocalNodeState)
printSwarmInfo(dockerCli, info)
if len(info.Runtimes) > 0 {
fmt.Fprintf(dockerCli.Out(), "Runtimes:")
fmt.Fprint(dockerCli.Out(), "Runtimes:")
for name := range info.Runtimes {
fmt.Fprintf(dockerCli.Out(), " %s", name)
}
fmt.Fprint(dockerCli.Out(), "\n")
fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime)
fmt.Fprintln(dockerCli.Out(), "Default Runtime:", info.DefaultRuntime)
}
if info.OSType == "linux" {
fmt.Fprintf(dockerCli.Out(), "Init Binary: %v\n", info.InitBinary)
fmt.Fprintln(dockerCli.Out(), "Init Binary:", info.InitBinary)
for _, ci := range []struct {
Name string
@@ -170,23 +112,23 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
if ci.Commit.ID != ci.Commit.Expected {
fmt.Fprintf(dockerCli.Out(), " (expected: %s)", ci.Commit.Expected)
}
fmt.Fprintf(dockerCli.Out(), "\n")
fmt.Fprint(dockerCli.Out(), "\n")
}
if len(info.SecurityOptions) != 0 {
kvs, err := types.DecodeSecurityOptions(info.SecurityOptions)
if err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "Security Options:\n")
fmt.Fprintln(dockerCli.Out(), "Security Options:")
for _, so := range kvs {
fmt.Fprintf(dockerCli.Out(), " %s\n", so.Name)
fmt.Fprintln(dockerCli.Out(), " "+so.Name)
for _, o := range so.Options {
switch o.Key {
case "profile":
if o.Value != "default" {
fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n")
fmt.Fprintln(dockerCli.Err(), " WARNING: You're not using the default seccomp profile")
}
fmt.Fprintf(dockerCli.Out(), " Profile: %s\n", o.Value)
fmt.Fprintln(dockerCli.Out(), " Profile:", o.Value)
}
}
}
@@ -195,44 +137,44 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
// Isolation only has meaning on a Windows daemon.
if info.OSType == "windows" {
fmt.Fprintf(dockerCli.Out(), "Default Isolation: %v\n", info.Isolation)
fmt.Fprintln(dockerCli.Out(), "Default Isolation:", info.Isolation)
}
fprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion)
fprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem)
fprintfIfNotEmpty(dockerCli.Out(), "OSType: %s\n", info.OSType)
fprintfIfNotEmpty(dockerCli.Out(), "Architecture: %s\n", info.Architecture)
fmt.Fprintf(dockerCli.Out(), "CPUs: %d\n", info.NCPU)
fmt.Fprintf(dockerCli.Out(), "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal)))
fprintfIfNotEmpty(dockerCli.Out(), "Name: %s\n", info.Name)
fprintfIfNotEmpty(dockerCli.Out(), "ID: %s\n", info.ID)
fmt.Fprintf(dockerCli.Out(), "Docker Root Dir: %s\n", info.DockerRootDir)
fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", debug.IsEnabled())
fmt.Fprintf(dockerCli.Out(), "Debug Mode (server): %v\n", info.Debug)
fprintlnNonEmpty(dockerCli.Out(), "Kernel Version:", info.KernelVersion)
fprintlnNonEmpty(dockerCli.Out(), "Operating System:", info.OperatingSystem)
fprintlnNonEmpty(dockerCli.Out(), "OSType:", info.OSType)
fprintlnNonEmpty(dockerCli.Out(), "Architecture:", info.Architecture)
fmt.Fprintln(dockerCli.Out(), "CPUs:", info.NCPU)
fmt.Fprintln(dockerCli.Out(), "Total Memory:", units.BytesSize(float64(info.MemTotal)))
fprintlnNonEmpty(dockerCli.Out(), "Name:", info.Name)
fprintlnNonEmpty(dockerCli.Out(), "ID:", info.ID)
fmt.Fprintln(dockerCli.Out(), "Docker Root Dir:", info.DockerRootDir)
fmt.Fprintln(dockerCli.Out(), "Debug Mode (client):", debug.IsEnabled())
fmt.Fprintln(dockerCli.Out(), "Debug Mode (server):", info.Debug)
if info.Debug {
fmt.Fprintf(dockerCli.Out(), " File Descriptors: %d\n", info.NFd)
fmt.Fprintf(dockerCli.Out(), " Goroutines: %d\n", info.NGoroutines)
fmt.Fprintf(dockerCli.Out(), " System Time: %s\n", info.SystemTime)
fmt.Fprintf(dockerCli.Out(), " EventsListeners: %d\n", info.NEventsListener)
fmt.Fprintln(dockerCli.Out(), " File Descriptors:", info.NFd)
fmt.Fprintln(dockerCli.Out(), " Goroutines:", info.NGoroutines)
fmt.Fprintln(dockerCli.Out(), " System Time:", info.SystemTime)
fmt.Fprintln(dockerCli.Out(), " EventsListeners:", info.NEventsListener)
}
fprintfIfNotEmpty(dockerCli.Out(), "Http Proxy: %s\n", info.HTTPProxy)
fprintfIfNotEmpty(dockerCli.Out(), "Https Proxy: %s\n", info.HTTPSProxy)
fprintfIfNotEmpty(dockerCli.Out(), "No Proxy: %s\n", info.NoProxy)
fprintlnNonEmpty(dockerCli.Out(), "HTTP Proxy:", info.HTTPProxy)
fprintlnNonEmpty(dockerCli.Out(), "HTTPS Proxy:", info.HTTPSProxy)
fprintlnNonEmpty(dockerCli.Out(), "No Proxy:", info.NoProxy)
if info.IndexServerAddress != "" {
u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username
if len(u) > 0 {
fmt.Fprintf(dockerCli.Out(), "Username: %v\n", u)
fmt.Fprintln(dockerCli.Out(), "Username:", u)
}
fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress)
fmt.Fprintln(dockerCli.Out(), "Registry:", info.IndexServerAddress)
}
if info.Labels != nil {
fmt.Fprintln(dockerCli.Out(), "Labels:")
for _, attribute := range info.Labels {
fmt.Fprintf(dockerCli.Out(), " %s\n", attribute)
for _, lbl := range info.Labels {
fmt.Fprintln(dockerCli.Out(), " "+lbl)
}
// TODO: Engine labels with duplicate keys has been deprecated in 1.13 and will be error out
// after 3 release cycles (17.12). For now, a WARNING will be generated. The following will
@@ -251,20 +193,15 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
}
}
fmt.Fprintf(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild)
if info.ClusterStore != "" {
fmt.Fprintf(dockerCli.Out(), "Cluster Store: %s\n", info.ClusterStore)
}
if info.ClusterAdvertise != "" {
fmt.Fprintf(dockerCli.Out(), "Cluster Advertise: %s\n", info.ClusterAdvertise)
}
fmt.Fprintln(dockerCli.Out(), "Experimental:", info.ExperimentalBuild)
fprintlnNonEmpty(dockerCli.Out(), "Cluster Store:", info.ClusterStore)
fprintlnNonEmpty(dockerCli.Out(), "Cluster Advertise:", info.ClusterAdvertise)
if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) {
fmt.Fprintln(dockerCli.Out(), "Insecure Registries:")
for _, registry := range info.RegistryConfig.IndexConfigs {
if !registry.Secure {
fmt.Fprintf(dockerCli.Out(), " %s\n", registry.Name)
fmt.Fprintln(dockerCli.Out(), " "+registry.Name)
}
}
@@ -277,11 +214,12 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 {
fmt.Fprintln(dockerCli.Out(), "Registry Mirrors:")
for _, mirror := range info.RegistryConfig.Mirrors {
fmt.Fprintf(dockerCli.Out(), " %s\n", mirror)
fmt.Fprintln(dockerCli.Out(), " "+mirror)
}
}
fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n\n", info.LiveRestoreEnabled)
fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled)
fmt.Fprint(dockerCli.Out(), "\n")
// Only output these warnings if the server does not support these features
if info.OSType != "windows" {
@@ -325,7 +263,64 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error {
return nil
}
func printStorageDriverWarnings(dockerCli *command.DockerCli, info types.Info) {
func printSwarmInfo(dockerCli command.Cli, info types.Info) {
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive || info.Swarm.LocalNodeState == swarm.LocalNodeStateLocked {
return
}
fmt.Fprintln(dockerCli.Out(), " NodeID:", info.Swarm.NodeID)
if info.Swarm.Error != "" {
fmt.Fprintln(dockerCli.Out(), " Error:", info.Swarm.Error)
}
fmt.Fprintln(dockerCli.Out(), " Is Manager:", info.Swarm.ControlAvailable)
if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError {
fmt.Fprintln(dockerCli.Out(), " ClusterID:", info.Swarm.Cluster.ID)
fmt.Fprintln(dockerCli.Out(), " Managers:", info.Swarm.Managers)
fmt.Fprintln(dockerCli.Out(), " Nodes:", info.Swarm.Nodes)
fmt.Fprintln(dockerCli.Out(), " Orchestration:")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
}
fmt.Fprintln(dockerCli.Out(), " Task History Retention Limit:", taskHistoryRetentionLimit)
fmt.Fprintln(dockerCli.Out(), " Raft:")
fmt.Fprintln(dockerCli.Out(), " Snapshot Interval:", info.Swarm.Cluster.Spec.Raft.SnapshotInterval)
if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil {
fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots)
}
fmt.Fprintln(dockerCli.Out(), " Heartbeat Tick:", info.Swarm.Cluster.Spec.Raft.HeartbeatTick)
fmt.Fprintln(dockerCli.Out(), " Election Tick:", info.Swarm.Cluster.Spec.Raft.ElectionTick)
fmt.Fprintln(dockerCli.Out(), " Dispatcher:")
fmt.Fprintln(dockerCli.Out(), " Heartbeat Period:", units.HumanDuration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))
fmt.Fprintln(dockerCli.Out(), " CA Configuration:")
fmt.Fprintln(dockerCli.Out(), " Expiry Duration:", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry))
fmt.Fprintln(dockerCli.Out(), " Force Rotate:", info.Swarm.Cluster.Spec.CAConfig.ForceRotate)
if caCert := strings.TrimSpace(info.Swarm.Cluster.Spec.CAConfig.SigningCACert); caCert != "" {
fmt.Fprintf(dockerCli.Out(), " Signing CA Certificate: \n%s\n\n", caCert)
}
if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 {
fmt.Fprintln(dockerCli.Out(), " External CAs:")
for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs {
fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL)
}
}
fmt.Fprintln(dockerCli.Out(), " Autolock Managers:", info.Swarm.Cluster.Spec.EncryptionConfig.AutoLockManagers)
fmt.Fprintln(dockerCli.Out(), " Root Rotation In Progress:", info.Swarm.Cluster.RootRotationInProgress)
}
fmt.Fprintln(dockerCli.Out(), " Node Address:", info.Swarm.NodeAddr)
if len(info.Swarm.RemoteManagers) > 0 {
managers := []string{}
for _, entry := range info.Swarm.RemoteManagers {
managers = append(managers, entry.Addr)
}
sort.Strings(managers)
fmt.Fprintln(dockerCli.Out(), " Manager Addresses:")
for _, entry := range managers {
fmt.Fprintf(dockerCli.Out(), " %s\n", entry)
}
}
}
func printStorageDriverWarnings(dockerCli command.Cli, info types.Info) {
if info.DriverStatus == nil {
return
}
@@ -373,9 +368,8 @@ func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) er
return err
}
func fprintfIfNotEmpty(w io.Writer, format, value string) (int, error) {
func fprintlnNonEmpty(w io.Writer, label, value string) {
if value != "" {
return fmt.Fprintf(w, format, value)
fmt.Fprintln(w, label, value)
}
return 0, nil
}

View File

@@ -0,0 +1,237 @@
package system
import (
"encoding/base64"
"net"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/stretchr/testify/assert"
)
// helper function that base64 decodes a string and ignores the error
func base64Decode(val string) []byte {
decoded, _ := base64.StdEncoding.DecodeString(val)
return decoded
}
var sampleInfoNoSwarm = types.Info{
ID: "EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX",
Containers: 0,
ContainersRunning: 0,
ContainersPaused: 0,
ContainersStopped: 0,
Images: 0,
Driver: "aufs",
DriverStatus: [][2]string{
{"Root Dir", "/var/lib/docker/aufs"},
{"Backing Filesystem", "extfs"},
{"Dirs", "0"},
{"Dirperm1 Supported", "true"},
},
SystemStatus: nil,
Plugins: types.PluginsInfo{
Volume: []string{"local"},
Network: []string{"bridge", "host", "macvlan", "null", "overlay"},
Authorization: nil,
Log: []string{"awslogs", "fluentd", "gcplogs", "gelf", "journald", "json-file", "logentries", "splunk", "syslog"},
},
MemoryLimit: true,
SwapLimit: true,
KernelMemory: true,
CPUCfsPeriod: true,
CPUCfsQuota: true,
CPUShares: true,
CPUSet: true,
IPv4Forwarding: true,
BridgeNfIptables: true,
BridgeNfIP6tables: true,
Debug: true,
NFd: 33,
OomKillDisable: true,
NGoroutines: 135,
SystemTime: "2017-08-24T17:44:34.077811894Z",
LoggingDriver: "json-file",
CgroupDriver: "cgroupfs",
NEventsListener: 0,
KernelVersion: "4.4.0-87-generic",
OperatingSystem: "Ubuntu 16.04.3 LTS",
OSType: "linux",
Architecture: "x86_64",
IndexServerAddress: "https://index.docker.io/v1/",
RegistryConfig: &registry.ServiceConfig{
AllowNondistributableArtifactsCIDRs: nil,
AllowNondistributableArtifactsHostnames: nil,
InsecureRegistryCIDRs: []*registry.NetIPNet{
{
IP: net.ParseIP("127.0.0.0"),
Mask: net.IPv4Mask(255, 0, 0, 0),
},
},
IndexConfigs: map[string]*registry.IndexInfo{
"docker.io": {
Name: "docker.io",
Mirrors: nil,
Secure: true,
Official: true,
},
},
Mirrors: nil,
},
NCPU: 2,
MemTotal: 2097356800,
DockerRootDir: "/var/lib/docker",
HTTPProxy: "",
HTTPSProxy: "",
NoProxy: "",
Name: "system-sample",
Labels: []string{"provider=digitalocean"},
ExperimentalBuild: false,
ServerVersion: "17.06.1-ce",
ClusterStore: "",
ClusterAdvertise: "",
Runtimes: map[string]types.Runtime{
"runc": {
Path: "docker-runc",
Args: nil,
},
},
DefaultRuntime: "runc",
Swarm: swarm.Info{LocalNodeState: "inactive"},
LiveRestoreEnabled: false,
Isolation: "",
InitBinary: "docker-init",
ContainerdCommit: types.Commit{
ID: "6e23458c129b551d5c9871e5174f6b1b7f6d1170",
Expected: "6e23458c129b551d5c9871e5174f6b1b7f6d1170",
},
RuncCommit: types.Commit{
ID: "810190ceaa507aa2727d7ae6f4790c76ec150bd2",
Expected: "810190ceaa507aa2727d7ae6f4790c76ec150bd2",
},
InitCommit: types.Commit{
ID: "949e6fa",
Expected: "949e6fa",
},
SecurityOptions: []string{"name=apparmor", "name=seccomp,profile=default"},
}
var sampleSwarmInfo = swarm.Info{
NodeID: "qo2dfdig9mmxqkawulggepdih",
NodeAddr: "165.227.107.89",
LocalNodeState: "active",
ControlAvailable: true,
Error: "",
RemoteManagers: []swarm.Peer{
{
NodeID: "qo2dfdig9mmxqkawulggepdih",
Addr: "165.227.107.89:2377",
},
},
Nodes: 1,
Managers: 1,
Cluster: &swarm.ClusterInfo{
ID: "9vs5ygs0gguyyec4iqf2314c0",
Meta: swarm.Meta{
Version: swarm.Version{Index: 11},
CreatedAt: time.Date(2017, 8, 24, 17, 34, 19, 278062352, time.UTC),
UpdatedAt: time.Date(2017, 8, 24, 17, 34, 42, 398815481, time.UTC),
},
Spec: swarm.Spec{
Annotations: swarm.Annotations{
Name: "default",
Labels: nil,
},
Orchestration: swarm.OrchestrationConfig{
TaskHistoryRetentionLimit: &[]int64{5}[0],
},
Raft: swarm.RaftConfig{
SnapshotInterval: 10000,
KeepOldSnapshots: &[]uint64{0}[0],
LogEntriesForSlowFollowers: 500,
ElectionTick: 3,
HeartbeatTick: 1,
},
Dispatcher: swarm.DispatcherConfig{
HeartbeatPeriod: 5000000000,
},
CAConfig: swarm.CAConfig{
NodeCertExpiry: 7776000000000000,
},
TaskDefaults: swarm.TaskDefaults{},
EncryptionConfig: swarm.EncryptionConfig{
AutoLockManagers: true,
},
},
TLSInfo: swarm.TLSInfo{
TrustRoot: `
-----BEGIN CERTIFICATE-----
MIIBajCCARCgAwIBAgIUaFCW5xsq8eyiJ+Pmcv3MCflMLnMwCgYIKoZIzj0EAwIw
EzERMA8GA1UEAxMIc3dhcm0tY2EwHhcNMTcwODI0MTcyOTAwWhcNMzcwODE5MTcy
OTAwWjATMREwDwYDVQQDEwhzd2FybS1jYTBZMBMGByqGSM49AgEGCCqGSM49AwEH
A0IABDy7NebyUJyUjWJDBUdnZoV6GBxEGKO4TZPNDwnxDxJcUdLVaB7WGa4/DLrW
UfsVgh1JGik2VTiLuTMA1tLlNPOjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
Af8EBTADAQH/MB0GA1UdDgQWBBQl16XFtaaXiUAwEuJptJlDjfKskDAKBggqhkjO
PQQDAgNIADBFAiEAo9fTQNM5DP9bHVcTJYfl2Cay1bFu1E+lnpmN+EYJfeACIGKH
1pCUkZ+D0IB6CiEZGWSHyLuXPM1rlP+I5KuS7sB8
-----END CERTIFICATE-----
`,
CertIssuerSubject: base64Decode("MBMxETAPBgNVBAMTCHN3YXJtLWNh"),
CertIssuerPublicKey: base64Decode(
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPLs15vJQnJSNYkMFR2dmhXoYHEQYo7hNk80PCfEPElxR0tVoHtYZrj8MutZR+xWCHUkaKTZVOIu5MwDW0uU08w=="),
},
RootRotationInProgress: false,
},
}
func TestPrettyPrintInfo(t *testing.T) {
infoWithSwarm := sampleInfoNoSwarm
infoWithSwarm.Swarm = sampleSwarmInfo
infoWithWarningsLinux := sampleInfoNoSwarm
infoWithWarningsLinux.MemoryLimit = false
infoWithWarningsLinux.SwapLimit = false
infoWithWarningsLinux.KernelMemory = false
infoWithWarningsLinux.OomKillDisable = false
infoWithWarningsLinux.CPUCfsQuota = false
infoWithWarningsLinux.CPUCfsPeriod = false
infoWithWarningsLinux.CPUShares = false
infoWithWarningsLinux.CPUSet = false
infoWithWarningsLinux.IPv4Forwarding = false
infoWithWarningsLinux.BridgeNfIptables = false
infoWithWarningsLinux.BridgeNfIP6tables = false
for _, tc := range []struct {
dockerInfo types.Info
expectedGolden string
warningsGolden string
}{
{
dockerInfo: sampleInfoNoSwarm,
expectedGolden: "docker-info-no-swarm",
},
{
dockerInfo: infoWithSwarm,
expectedGolden: "docker-info-with-swarm",
},
{
dockerInfo: infoWithWarningsLinux,
expectedGolden: "docker-info-no-swarm",
warningsGolden: "docker-info-warnings",
},
} {
cli := test.NewFakeCli(&fakeClient{})
assert.NoError(t, prettyPrintInfo(cli, tc.dockerInfo))
golden.Assert(t, cli.OutBuffer().String(), tc.expectedGolden+".golden")
if tc.warningsGolden != "" {
golden.Assert(t, cli.ErrBuffer().String(), tc.warningsGolden+".golden")
} else {
assert.Equal(t, "", cli.ErrBuffer().String())
}
}
}

View File

@@ -0,0 +1,51 @@
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.06.1-ce
Storage Driver: aufs
Root Dir: /var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 0
Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 6e23458c129b551d5c9871e5174f6b1b7f6d1170
runc version: 810190ceaa507aa2727d7ae6f4790c76ec150bd2
init version: 949e6fa
Security Options:
apparmor
seccomp
Profile: default
Kernel Version: 4.4.0-87-generic
Operating System: Ubuntu 16.04.3 LTS
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.953GiB
Name: system-sample
ID: EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
File Descriptors: 33
Goroutines: 135
System Time: 2017-08-24T17:44:34.077811894Z
EventsListeners: 0
Registry: https://index.docker.io/v1/
Labels:
provider=digitalocean
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false

View File

@@ -0,0 +1,11 @@
WARNING: No memory limit support
WARNING: No swap limit support
WARNING: No kernel memory limit support
WARNING: No oom kill disable support
WARNING: No cpu cfs quota support
WARNING: No cpu cfs period support
WARNING: No cpu shares support
WARNING: No cpuset support
WARNING: IPv4 forwarding is disabled
WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled

View File

@@ -0,0 +1,73 @@
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.06.1-ce
Storage Driver: aufs
Root Dir: /var/lib/docker/aufs
Backing Filesystem: extfs
Dirs: 0
Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: active
NodeID: qo2dfdig9mmxqkawulggepdih
Is Manager: true
ClusterID: 9vs5ygs0gguyyec4iqf2314c0
Managers: 1
Nodes: 1
Orchestration:
Task History Retention Limit: 5
Raft:
Snapshot Interval: 10000
Number of Old Snapshots to Retain: 0
Heartbeat Tick: 1
Election Tick: 3
Dispatcher:
Heartbeat Period: 5 seconds
CA Configuration:
Expiry Duration: 3 months
Force Rotate: 0
Autolock Managers: true
Root Rotation In Progress: false
Node Address: 165.227.107.89
Manager Addresses:
165.227.107.89:2377
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 6e23458c129b551d5c9871e5174f6b1b7f6d1170
runc version: 810190ceaa507aa2727d7ae6f4790c76ec150bd2
init version: 949e6fa
Security Options:
apparmor
seccomp
Profile: default
Kernel Version: 4.4.0-87-generic
Operating System: Ubuntu 16.04.3 LTS
OSType: linux
Architecture: x86_64
CPUs: 2
Total Memory: 1.953GiB
Name: system-sample
ID: EKHL:QDUU:QZ7U:MKGD:VDXK:S27Q:GIPU:24B7:R7VT:DGN6:QCSF:2UBX
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): true
File Descriptors: 33
Goroutines: 135
System Time: 2017-08-24T17:44:34.077811894Z
EventsListeners: 0
Registry: https://index.docker.io/v1/
Labels:
provider=digitalocean
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false

View File

@@ -12,6 +12,10 @@ var (
untrusted bool
)
func init() {
untrusted = !getDefaultTrustState()
}
// AddTrustVerificationFlags adds content trust flags to the provided flagset
func AddTrustVerificationFlags(fs *pflag.FlagSet) {
trusted := getDefaultTrustState()

View File

@@ -128,7 +128,7 @@ func Service(
Labels: AddStackLabel(namespace, service.Deploy.Labels),
},
TaskTemplate: swarm.TaskSpec{
ContainerSpec: swarm.ContainerSpec{
ContainerSpec: &swarm.ContainerSpec{
Image: service.Image,
Command: service.Entrypoint,
Args: service.Command,
@@ -295,7 +295,13 @@ func convertServiceSecrets(
})
}
return servicecli.ParseSecrets(client, refs)
secrs, err := servicecli.ParseSecrets(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
return secrs, err
}
// TODO: fix configs API so that ConfigsAPIClient is not required here
@@ -346,7 +352,13 @@ func convertServiceConfigObjs(
})
}
return servicecli.ParseConfigs(client, refs)
confs, err := servicecli.ParseConfigs(client, refs)
if err != nil {
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
return confs, err
}
func uint32Ptr(value uint32) *uint32 {
@@ -366,7 +378,6 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container
return nil, nil
}
var (
err error
timeout, interval, startPeriod time.Duration
retries int
)
@@ -379,23 +390,14 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container
}, nil
}
if healthcheck.Timeout != "" {
timeout, err = time.ParseDuration(healthcheck.Timeout)
if err != nil {
return nil, err
}
if healthcheck.Timeout != nil {
timeout = *healthcheck.Timeout
}
if healthcheck.Interval != "" {
interval, err = time.ParseDuration(healthcheck.Interval)
if err != nil {
return nil, err
}
if healthcheck.Interval != nil {
interval = *healthcheck.Interval
}
if healthcheck.StartPeriod != "" {
startPeriod, err = time.ParseDuration(healthcheck.StartPeriod)
if err != nil {
return nil, err
}
if healthcheck.StartPeriod != nil {
startPeriod = *healthcheck.StartPeriod
}
if healthcheck.Retries != nil {
retries = int(*healthcheck.Retries)

View File

@@ -109,16 +109,18 @@ func TestConvertResourcesOnlyMemory(t *testing.T) {
func TestConvertHealthcheck(t *testing.T) {
retries := uint64(10)
timeout := 30 * time.Second
interval := 2 * time.Millisecond
source := &composetypes.HealthCheckConfig{
Test: []string{"EXEC", "touch", "/foo"},
Timeout: "30s",
Interval: "2ms",
Timeout: &timeout,
Interval: &interval,
Retries: &retries,
}
expected := &container.HealthConfig{
Test: source.Test,
Timeout: 30 * time.Second,
Interval: 2 * time.Millisecond,
Timeout: timeout,
Interval: interval,
Retries: 10,
}

View File

@@ -2,6 +2,21 @@ version: "3.4"
services:
foo:
build:
context: ./dir
dockerfile: Dockerfile
args:
foo: bar
target: foo
network: foo
cache_from:
- foo
- bar
labels: [FOO=BAR]
cap_add:
- ALL
@@ -114,6 +129,7 @@ services:
interval: 10s
timeout: 1s
retries: 5
start_period: 15s
# Any valid image reference - repo, tag, id, sha
image: redis

View File

@@ -8,7 +8,6 @@ import (
"sort"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/compose/interpolation"
"github.com/docker/cli/cli/compose/schema"
"github.com/docker/cli/cli/compose/template"
@@ -19,6 +18,7 @@ import (
shellwords "github.com/mattn/go-shellwords"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
@@ -93,11 +93,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
}
cfg.Configs, err = LoadConfigObjs(config["configs"], configDetails.WorkingDir)
if err != nil {
return nil, err
}
return &cfg, nil
return &cfg, err
}
func interpolateConfig(configDict map[string]interface{}, lookupEnv template.Mapping) (map[string]map[string]interface{}, error) {
@@ -576,7 +572,6 @@ func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
default:
return data, errors.Errorf("invalid type %T for service volume", value)
}
}
func transformServiceNetworkMap(value interface{}) (interface{}, error) {

View File

@@ -517,12 +517,14 @@ version: "3"
services:
web:
image: web
build: ./web
build:
context: ./web
links:
- bar
db:
image: db
build: ./db
build:
context: ./db
`))
assert.NoError(t, err)
@@ -686,6 +688,15 @@ func TestFullExample(t *testing.T) {
expectedServiceConfig := types.ServiceConfig{
Name: "foo",
Build: types.BuildConfig{
Context: "./dir",
Dockerfile: "Dockerfile",
Args: map[string]*string{"foo": strPtr("bar")},
Target: "foo",
Network: "foo",
CacheFrom: []string{"foo", "bar"},
Labels: map[string]string{"FOO": "BAR"},
},
CapAdd: []string{"ALL"},
CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"},
CgroupParent: "m-executor-abcd",
@@ -756,10 +767,11 @@ func TestFullExample(t *testing.T) {
"somehost": "162.242.195.82",
},
HealthCheck: &types.HealthCheckConfig{
Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}),
Interval: "10s",
Timeout: "1s",
Retries: uint64Ptr(5),
Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}),
Interval: durationPtr(10 * time.Second),
Timeout: durationPtr(1 * time.Second),
Retries: uint64Ptr(5),
StartPeriod: durationPtr(15 * time.Second),
},
Hostname: "foo",
Image: "redis",

View File

@@ -200,3 +200,13 @@ func TestParseVolumeSplitCases(t *testing.T) {
assert.Equal(t, expected, parsed.Source != "", msg)
}
}
func TestParseVolumeInvalidEmptySpec(t *testing.T) {
_, err := ParseVolume("")
testutil.ErrorContains(t, err, "invalid empty volume spec")
}
func TestParseVolumeInvalidSections(t *testing.T) {
_, err := ParseVolume("/foo::rw")
testutil.ErrorContains(t, err, "invalid spec")
}

File diff suppressed because one or more lines are too long

View File

@@ -36,6 +36,16 @@ func TestValidateUndefinedTopLevelOption(t *testing.T) {
assert.Contains(t, err.Error(), "Additional property helicopters is not allowed")
}
func TestValidateAllowsXTopLevelFields(t *testing.T) {
config := dict{
"version": "3.4",
"x-extra-stuff": dict{},
}
err := Validate(config, "3.4")
assert.NoError(t, err)
}
func TestValidateInvalidVersion(t *testing.T) {
config := dict{
"version": "2.1",

View File

@@ -23,6 +23,7 @@ var UnsupportedProperties = []string{
"shm_size",
"sysctls",
"tmpfs",
"ulimits",
"userns_mode",
}
@@ -78,6 +79,7 @@ type Config struct {
type ServiceConfig struct {
Name string
Build BuildConfig
CapAdd []string `mapstructure:"cap_add"`
CapDrop []string `mapstructure:"cap_drop"`
CgroupParent string `mapstructure:"cgroup_parent"`
@@ -125,6 +127,18 @@ type ServiceConfig struct {
WorkingDir string `mapstructure:"working_dir"`
}
// BuildConfig is a type for build
// using the same format at libcompose: https://github.com/docker/libcompose/blob/master/yaml/build.go#L12
type BuildConfig struct {
Context string
Dockerfile string
Args MappingWithEquals
Labels Labels
CacheFrom StringList `mapstructure:"cache_from"`
Network string
Target string
}
// ShellCommand is a string or list of string args
type ShellCommand []string
@@ -169,10 +183,10 @@ type DeployConfig struct {
// HealthCheckConfig the healthcheck configuration for a service
type HealthCheckConfig struct {
Test HealthCheckTest
Timeout string
Interval string
Timeout *time.Duration
Interval *time.Duration
Retries *uint64
StartPeriod string
StartPeriod *time.Duration `mapstructure:"start_period"`
Disable bool
}

View File

@@ -3,7 +3,7 @@ package debug
import (
"os"
"github.com/Sirupsen/logrus"
"github.com/sirupsen/logrus"
)
// Enable sets the DEBUG env var to true

View File

@@ -4,7 +4,7 @@ import (
"os"
"testing"
"github.com/Sirupsen/logrus"
"github.com/sirupsen/logrus"
)
func TestEnable(t *testing.T) {

View File

@@ -5,10 +5,10 @@ import (
"os"
"path/filepath"
"github.com/Sirupsen/logrus"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/opts"
"github.com/docker/go-connections/tlsconfig"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
)

View File

@@ -10,7 +10,6 @@ import (
"path/filepath"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/distribution/registry/client/auth"
@@ -29,6 +28,7 @@ import (
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
var (

View File

@@ -6,7 +6,6 @@ import (
"os"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/commands"
@@ -16,6 +15,7 @@ import (
"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"
)

View File

@@ -5,9 +5,9 @@ import (
"os"
"testing"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)

View File

@@ -441,18 +441,23 @@ __docker_complete_stacks() {
# precedence over the environment setting.
# Completions may be added with `--add`, e.g. `--add self`.
__docker_nodes() {
local format
if [ "$DOCKER_COMPLETION_SHOW_NODE_IDS" = yes ] ; then
format='{{.ID}} {{.Hostname}}'
else
format='{{.Hostname}}'
fi
local add=()
local fields='$2' # default: node name only
[ "${DOCKER_COMPLETION_SHOW_NODE_IDS}" = yes ] && fields='$1,$2' # ID and name
while true ; do
case "$1" in
--id)
fields='$1' # IDs only
format='{{.ID}}'
shift
;;
--name)
fields='$2' # names only
format='{{.Hostname}}'
shift
;;
--add)
@@ -465,7 +470,7 @@ __docker_nodes() {
esac
done
echo "$(__docker_q node ls "$@" | tr -d '*' | awk "NR>1 {print $fields}")" "${add[@]}"
echo "$(__docker_q node ls --format "$format" "$@")" "${add[@]}"
}
# __docker_complete_nodes applies completion of nodes based on the current
@@ -1862,7 +1867,7 @@ _docker_container_run_and_create() {
__docker_complete_containers_running
;;
*)
COMPREPLY=( $( compgen -W 'host container:' -- "$cur" ) )
COMPREPLY=( $( compgen -W 'none host private shareable container:' -- "$cur" ) )
# shellcheck disable=SC2128
if [ "$COMPREPLY" = "container:" ]; then
__docker_nospace

View File

@@ -4,10 +4,10 @@
# Makefile for developing using Docker
#
DEV_DOCKER_IMAGE_NAME = docker-cli-dev
LINTER_IMAGE_NAME = docker-cli-lint
CROSS_IMAGE_NAME = docker-cli-cross
VALIDATE_IMAGE_NAME = docker-cli-shell-validate
DEV_DOCKER_IMAGE_NAME = docker-cli-dev$(IMAGE_TAG)
LINTER_IMAGE_NAME = docker-cli-lint$(IMAGE_TAG)
CROSS_IMAGE_NAME = docker-cli-cross$(IMAGE_TAG)
VALIDATE_IMAGE_NAME = docker-cli-shell-validate$(IMAGE_TAG)
MOUNTS = -v "$(CURDIR)":/go/src/github.com/docker/cli
VERSION = $(shell cat VERSION)
ENVVARS = -e VERSION=$(VERSION) -e GITCOMMIT
@@ -42,9 +42,12 @@ clean: build_docker_image
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make clean
# run go test
.PHONY: test-unit
test-unit: build_docker_image
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make test-unit
.PHONY: test
test: build_docker_image
docker run --rm $(ENVVARS) $(MOUNTS) $(DEV_DOCKER_IMAGE_NAME) make test
test: test-unit test-e2e
# build the CLI for multiple architectures using a container
.PHONY: cross
@@ -90,3 +93,7 @@ yamldocs: build_docker_image
.PHONY: shellcheck
shellcheck: build_shell_validate_image
docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck
.PHONY: test-e2e
test-e2e: binary
./scripts/test/e2e/wrapper

View File

@@ -0,0 +1,17 @@
FROM docker/compose:1.15.0
RUN apk add -U bash curl
ARG DOCKER_CHANNEL=edge
ARG DOCKER_VERSION=17.06.0-ce
RUN export URL=https://download.docker.com/linux/static; \
curl -Ls $URL/$DOCKER_CHANNEL/x86_64/docker-$DOCKER_VERSION.tgz | \
tar -xz docker/docker && \
mv docker/docker /usr/local/bin/ && \
rmdir docker
ENV DISABLE_WARN_OUTSIDE_CONTAINER=1
WORKDIR /work
COPY scripts/test/e2e scripts/test/e2e
COPY e2e/compose-env.yaml e2e/compose-env.yaml
ENTRYPOINT ["bash", "/work/scripts/test/e2e/run"]

View File

@@ -24,7 +24,7 @@ see [Feature Deprecation Policy](https://docs.docker.com/engine/#feature-depreca
**Deprecated In Release: v17.05.0**
**Disabled by default in release: v17.09**
**Disabled by default in release: [v17.09](https://github.com/docker/docker-ce/releases/tag/v17.09.0-ce)**
Docker 17.05.0 added an optional `--detach=false` option to make the
`docker service create` and `docker service update` work synchronously. This
@@ -315,7 +315,7 @@ Since 1.9, Docker Content Trust Offline key has been renamed to Root key and the
**Deprecated In Release: [v1.6.0](https://github.com/docker/docker/releases/tag/v1.6.0)**
**Target For Removal In Release: v17.09**
**Removed In Release: [v17.09](https://github.com/docker/docker-ce/releases/tag/v17.09.0-ce)**
The flag `--api-enable-cors` is deprecated since v1.6.0. Use the flag
`--api-cors-header` instead.

Some files were not shown because too many files have changed in this diff Show More