Compare commits
5 Commits
v17.07.0-c
...
v17.06.0-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f8486a39a | |||
| 5f615dd295 | |||
| f3810787c8 | |||
| 2bcfe6ffc2 | |||
| 0b4548c769 |
63
CHANGELOG.md
63
CHANGELOG.md
@ -5,69 +5,6 @@ information on the list of deprecated flags and APIs please have a look at
|
||||
https://docs.docker.com/engine/deprecated/ where target removal dates can also
|
||||
be found.
|
||||
|
||||
## 17.07.0-ce (2017-08-XX)
|
||||
|
||||
### API & Client
|
||||
|
||||
* Add support for proxy configuration in config.json [docker/cli#93](https://github.com/docker/cli/pull/93)
|
||||
* Enable pprof/debug endpoints by default [moby/moby#32453](https://github.com/moby/moby/pull/32453)
|
||||
* Passwords can now be passed using `STDIN` using the new `--password-stdin` flag on `docker login` [docker/cli#271](https://github.com/docker/cli/pull/271)
|
||||
+ Add `--detach` to docker scale [docker/cli#243](https://github.com/docker/cli/pull/243)
|
||||
* Prevent `docker logs --no-stream` from hanging due to non-existing containers [moby/moby#34004](https://github.com/moby/moby/pull/34004)
|
||||
- Fix `docker stack ps` printing error to `stdout` instead of `stderr` [docker/cli#298](https://github.com/docker/cli/pull/298)
|
||||
* Fix progress bar being stuck on `docker service create` if an error occurs during deploy [docker/cli#259](https://github.com/docker/cli/pull/259)
|
||||
* Improve presentation of progress bars in interactive mode [docker/cli#260](https://github.com/docker/cli/pull/260) [docker/cli#237](https://github.com/docker/cli/pull/237)
|
||||
* Print a warning if `docker login --password` is used, and recommend `--password-stdin` [docker/cli#270](https://github.com/docker/cli/pull/270)
|
||||
* Make API version negotiation more robust [moby/moby#33827](https://github.com/moby/moby/pull/33827)
|
||||
* Hide `--detach` when connected to daemons older than Docker 17.05 [docker/cli#219](https://github.com/docker/cli/pull/219)
|
||||
+ Add `scope` filter in `GET /networks/(id or name)` [moby/moby#33630](https://github.com/moby/moby/pull/33630)
|
||||
|
||||
### Builder
|
||||
|
||||
* Implement long running interactive session and sending build context incrementally [moby/moby#32677](https://github.com/moby/moby/pull/32677) [docker/cli#231](https://github.com/docker/cli/pull/231) [moby/moby#33859](https://github.com/moby/moby/pull/33859)
|
||||
* Warn on empty continuation lines [moby/moby#33719](https://github.com/moby/moby/pull/33719)
|
||||
- Fix `.dockerignore` entries with a leading `/` not matching anything [moby/moby#32088](https://github.com/moby/moby/pull/32088)
|
||||
|
||||
### Logging
|
||||
|
||||
- Fix wrong filemode for rotate log files [moby/moby#33926](https://github.com/moby/moby/pull/33926)
|
||||
- Fix stderr logging for journald and syslog [moby/moby#33832](https://github.com/moby/moby/pull/33832)
|
||||
|
||||
### Runtime
|
||||
|
||||
* Allow stopping of paused container [moby/moby#34027](https://github.com/moby/moby/pull/34027)
|
||||
+ Add quota support for the overlay2 storage driver [moby/moby#32977](https://github.com/moby/moby/pull/32977)
|
||||
* Remove container locks on `docker ps` [moby/moby#31273](https://github.com/moby/moby/pull/31273)
|
||||
* Store container names in memdb [moby/moby#33886](https://github.com/moby/moby/pull/33886)
|
||||
* Fix race condition between `docker exec` and `docker pause` [moby/moby#32881](https://github.com/moby/moby/pull/32881)
|
||||
* Devicemapper: Rework logging and add `--storage-opt dm.libdm_log_level` [moby/moby#33845](https://github.com/moby/moby/pull/33845)
|
||||
* Devicemapper: Prevent "device in use" errors if deferred removal is enabled, but not deferred deletion [moby/moby#33877](https://github.com/moby/moby/pull/33877)
|
||||
* Devicemapper: Use KeepAlive to prevent tasks being garbage-collected while still in use [moby/moby#33376](https://github.com/moby/moby/pull/33376)
|
||||
* Report inetermediate prune results if prune is cancelled [moby/moby#33979](https://github.com/moby/moby/pull/33979)
|
||||
- Fix run `docker rename <container-id> new_name` concurrently resulting in the having multiple names [moby/moby#33940](https://github.com/moby/moby/pull/33940)
|
||||
* Fix file-descriptor leak and error handling [moby/moby#33713](https://github.com/moby/moby/pull/33713)
|
||||
- Fix SIGSEGV when running containers [docker/cli#303](https://github.com/docker/cli/pull/303)
|
||||
* Prevent a goroutine leak when healthcheck gets stopped [moby/moby#33781](https://github.com/moby/moby/pull/33781)
|
||||
* Image: Improve store locking [moby/moby#33755](https://github.com/moby/moby/pull/33755)
|
||||
* Fix Btrfs quota groups not being removed when container is destroyed [moby/moby#29427](https://github.com/moby/moby/pull/29427)
|
||||
* Libcontainerd: fix defunct containerd processes not being properly reaped [moby/moby#33419](https://github.com/moby/moby/pull/33419)
|
||||
* Preparations for Linux Containers on Windows
|
||||
* LCOW: Dedicated scratch space for service VM utilities [moby/moby#33809](https://github.com/moby/moby/pull/33809)
|
||||
* LCOW: Support most operations excluding remote filesystem [moby/moby#33241](https://github.com/moby/moby/pull/33241) [moby/moby#33826](https://github.com/moby/moby/pull/33826)
|
||||
* LCOW: Change directory from lcow to "Linux Containers" [moby/moby#33835](https://github.com/moby/moby/pull/33835)
|
||||
* LCOW: pass command arguments without extra quoting [moby/moby#33815](https://github.com/moby/moby/pull/33815)
|
||||
* LCOW: Updates necessary due to platform schema change [moby/moby#33785](https://github.com/moby/moby/pull/33785)
|
||||
- Aufs: ensure diff layers are correctly removed to prevent leftover files from using up storage [moby/moby#34587](https://github.com/moby/moby/pull/34587)
|
||||
|
||||
### Swarm Mode
|
||||
|
||||
* Initial support for plugable secret backends [moby/moby#34157](https://github.com/moby/moby/pull/34157) [moby/moby#34123](https://github.com/moby/moby/pull/34123)
|
||||
* Sort swarm stacks and nodes using natural sorting [docker/cli#315](https://github.com/docker/cli/pull/315)
|
||||
* Make engine support cluster config event [moby/moby#34032](https://github.com/moby/moby/pull/34032)
|
||||
* Only pass a join address when in the process of joining a cluster [moby/moby#33361](https://github.com/moby/moby/pull/33361)
|
||||
* Fix error during service creation if a network with the same name exists both as "local" and "swarm" scoped network [docker/cli#184](https://github.com/docker/cli/pull/184)
|
||||
* (experimental) Add support for plugins on swarm [moby/moby#33575](https://github.com/moby/moby/pull/33575)
|
||||
|
||||
## 17.06.0-ce (2017-06-07)
|
||||
|
||||
### Builder
|
||||
|
||||
@ -4,4 +4,3 @@ Want to contribute on Docker CE? Awesome!
|
||||
|
||||
* Changes to the `engine` should be directed upstream to https://github.com/moby/moby
|
||||
* Changes to the `cli` should be directed upstream to https://github.com/docker/cli
|
||||
* Changes to the `packaging` should be directed upstream to https://github.com/docker/docker-ce-packaging
|
||||
|
||||
3
Makefile
3
Makefile
@ -25,6 +25,3 @@ clean: ## clean the build artifacts
|
||||
-$(MAKE) -C $(CLI_DIR) clean
|
||||
-$(MAKE) -C $(ENGINE_DIR) clean
|
||||
-$(MAKE) -C $(PACKAGING_DIR) clean
|
||||
|
||||
vendor: $(CLI_DIR)/build/docker
|
||||
docker run --rm -it -v $(CLI_DIR):/go/src/github.com/docker/cli -v $(ENGINE_DIR):/go/src/github.com/docker/docker docker-cli-dev sh -c 'cd /go/src/github.com/docker/docker && git init && git add . && git -c user.name=user -c user.email=email@example.com commit -m first && cd /go/src/github.com/docker/cli && vndr; rm -rf /go/src/github.com/docker/docker/.git'
|
||||
|
||||
20
README.md
20
README.md
@ -1,24 +1,12 @@
|
||||
# Docker CE
|
||||
|
||||
This repository hosts open source components of Docker CE products. The
|
||||
`master` branch serves to unify the upstream components on a regular
|
||||
basis. Long-lived release branches host the code that goes into a product
|
||||
version for the lifetime of the product.
|
||||
This repository hosts open source components of Docker Community Edition
|
||||
(CE) products. The `master` branch serves to unify the upstream components
|
||||
on a regular basis. Long-lived release branches host the code that goes
|
||||
into a product version for the lifetime of the product.
|
||||
|
||||
This repository is solely maintained by Docker, Inc.
|
||||
|
||||
## Issues
|
||||
|
||||
There are separate issue-tracking repos for the end user Docker CE
|
||||
products specialized for a platform. Find your issue or file a new issue
|
||||
for the platform you are using:
|
||||
|
||||
* https://github.com/docker/for-linux
|
||||
* https://github.com/docker/for-mac
|
||||
* https://github.com/docker/for-win
|
||||
* https://github.com/docker/for-aws
|
||||
* https://github.com/docker/for-azure
|
||||
|
||||
## Unifying upstream sources
|
||||
|
||||
The `master` branch is a combination of components adapted from
|
||||
|
||||
8
components/cli/.github/CODEOWNERS
vendored
8
components/cli/.github/CODEOWNERS
vendored
@ -1,8 +0,0 @@
|
||||
# Github code owners
|
||||
# See https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
cli/compose/** @dnephin @vdemeester
|
||||
contrib/completion/bash/** @albers
|
||||
contrib/completion/zsh/** @sdurrheimer
|
||||
docs/** @mstanleyjones @vdemeester @thaJeztah
|
||||
scripts/** @dnephin
|
||||
6
components/cli/.gitignore
vendored
6
components/cli/.gitignore
vendored
@ -2,9 +2,3 @@
|
||||
/build/
|
||||
cli/winresources/rsrc_386.syso
|
||||
cli/winresources/rsrc_amd64.syso
|
||||
/man/man1/
|
||||
/man/man5/
|
||||
/man/man8/
|
||||
/docs/yaml/gen/
|
||||
coverage.txt
|
||||
profile.out
|
||||
|
||||
@ -1,66 +1,49 @@
|
||||
#
|
||||
# github.com/docker/cli
|
||||
#
|
||||
|
||||
all: binary
|
||||
|
||||
# remove build artifacts
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
|
||||
@rm -rf ./build/* cli/winresources/rsrc_*
|
||||
|
||||
# run go test
|
||||
# the "-tags daemon" part is temporary
|
||||
.PHONY: test
|
||||
test:
|
||||
./scripts/test/unit $(shell go list ./... | grep -v '/vendor/')
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage:
|
||||
./scripts/test/unit-with-coverage $(shell go list ./... | grep -v '/vendor/')
|
||||
@go test -tags daemon -v $(shell go list ./... | grep -v /vendor/)
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
gometalinter --config gometalinter.json ./...
|
||||
@gometalinter --config gometalinter.json ./...
|
||||
|
||||
.PHONY: binary
|
||||
binary:
|
||||
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
|
||||
./scripts/build/binary
|
||||
@./scripts/build/binary
|
||||
|
||||
# build the CLI for multiple architectures
|
||||
.PHONY: cross
|
||||
cross:
|
||||
./scripts/build/cross
|
||||
@./scripts/build/cross
|
||||
|
||||
.PHONY: dynbinary
|
||||
dynbinary:
|
||||
./scripts/build/dynbinary
|
||||
@./scripts/build/dynbinary
|
||||
|
||||
.PHONY: watch
|
||||
watch:
|
||||
./scripts/test/watch
|
||||
@./scripts/test/watch
|
||||
|
||||
# Check vendor matches vendor.conf
|
||||
# download dependencies (vendor/) listed in vendor.conf
|
||||
.PHONY: vendor
|
||||
vendor: vendor.conf
|
||||
vndr 2> /dev/null
|
||||
scripts/validate/check-git-diff vendor
|
||||
|
||||
## generate man pages from go source and markdown
|
||||
.PHONY: manpages
|
||||
manpages:
|
||||
scripts/docs/generate-man.sh
|
||||
|
||||
## generate documentation YAML files consumed by docs repo
|
||||
.PHONY: yamldocs
|
||||
yamldocs:
|
||||
scripts/docs/generate-yaml.sh
|
||||
|
||||
## Shellcheck validation
|
||||
.PHONY: shellcheck
|
||||
shellcheck:
|
||||
scripts/validate/shellcheck
|
||||
@vndr 2> /dev/null
|
||||
@scripts/validate/check-git-diff vendor
|
||||
|
||||
cli/compose/schema/bindata.go: cli/compose/schema/data/*.json
|
||||
go generate github.com/docker/cli/cli/compose/schema
|
||||
|
||||
compose-jsonschema: cli/compose/schema/bindata.go
|
||||
scripts/validate/check-git-diff cli/compose/schema/bindata.go
|
||||
@scripts/validate/check-git-diff cli/compose/schema/bindata.go
|
||||
|
||||
@ -1 +1 @@
|
||||
17.07.0-ce-rc4
|
||||
17.06.0-ce-rc1
|
||||
|
||||
@ -1,115 +1,49 @@
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
|
||||
lint:
|
||||
build:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
docker:
|
||||
- image: docker:17.05
|
||||
parallelism: 4
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
command: docker version
|
||||
- run:
|
||||
name: "Lint"
|
||||
command: |
|
||||
dockerfile=dockerfiles/Dockerfile.lint
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-linter:$CIRCLE_BUILD_NUM .
|
||||
docker run --rm cli-linter:$CIRCLE_BUILD_NUM
|
||||
|
||||
cross:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
parallelism: 3
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Cross"
|
||||
command: |
|
||||
dockerfile=dockerfiles/Dockerfile.cross
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder:$CIRCLE_BUILD_NUM .
|
||||
name=cross-$CIRCLE_BUILD_NUM-$CIRCLE_NODE_INDEX
|
||||
docker run \
|
||||
-e CROSS_GROUP=$CIRCLE_NODE_INDEX \
|
||||
--name $name cli-builder:$CIRCLE_BUILD_NUM \
|
||||
make cross
|
||||
docker cp \
|
||||
$name:/go/src/github.com/docker/cli/build \
|
||||
/work/build
|
||||
- store_artifacts:
|
||||
path: /work/build
|
||||
|
||||
test:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Unit Test with Coverage"
|
||||
command: |
|
||||
dockerfile=dockerfiles/Dockerfile.dev
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder:$CIRCLE_BUILD_NUM .
|
||||
docker run --name \
|
||||
test-$CIRCLE_BUILD_NUM cli-builder:$CIRCLE_BUILD_NUM \
|
||||
make test-coverage
|
||||
|
||||
- run:
|
||||
name: "Upload to Codecov"
|
||||
command: |
|
||||
docker cp \
|
||||
test-$CIRCLE_BUILD_NUM:/go/src/github.com/docker/cli/coverage.txt \
|
||||
coverage.txt
|
||||
apk add -U bash curl
|
||||
curl -s https://codecov.io/bash | bash
|
||||
|
||||
validate:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
reusable: true
|
||||
exclusive: false
|
||||
- run:
|
||||
name: "Validate Vendor, Docs, and Code Generation"
|
||||
command: |
|
||||
dockerfile=dockerfiles/Dockerfile.dev
|
||||
echo "COPY . ." >> $dockerfile
|
||||
rm -f .dockerignore # include .git
|
||||
docker build -f $dockerfile --tag cli-builder-with-git:$CIRCLE_BUILD_NUM .
|
||||
docker run --rm cli-builder-with-git:$CIRCLE_BUILD_NUM \
|
||||
make -B vendor compose-jsonschema manpages yamldocs
|
||||
shellcheck:
|
||||
working_directory: /work
|
||||
docker: [{image: 'docker:17.06-git'}]
|
||||
steps:
|
||||
name: "Install Git and SSH"
|
||||
command: apk add -U git openssh
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: "Run shellcheck"
|
||||
name: "Lint"
|
||||
command: |
|
||||
dockerfile=dockerfiles/Dockerfile.shellcheck
|
||||
if [ "$CIRCLE_NODE_INDEX" != "0" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.lint
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-validator:$CIRCLE_BUILD_NUM .
|
||||
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
|
||||
make -B shellcheck
|
||||
workflows:
|
||||
version: 2
|
||||
ci:
|
||||
jobs:
|
||||
- lint
|
||||
- cross
|
||||
- test
|
||||
- validate
|
||||
- shellcheck
|
||||
docker build -f $dockerfile --tag cli-linter .
|
||||
docker run cli-linter
|
||||
- run:
|
||||
name: "Cross"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "1" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.cross
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder .
|
||||
docker run --name cross cli-builder make cross
|
||||
docker cp cross:/go/src/github.com/docker/cli/build /work/build
|
||||
- run:
|
||||
name: "Unit Test"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "2" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.dev
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder .
|
||||
docker run cli-builder make test
|
||||
- run:
|
||||
name: "Validate Vendor and Code Generation"
|
||||
command: |
|
||||
if [ "$CIRCLE_NODE_INDEX" != "3" ]; then exit; fi
|
||||
dockerfile=dockerfiles/Dockerfile.dev
|
||||
echo "COPY . ." >> $dockerfile
|
||||
docker build -f $dockerfile --tag cli-builder .
|
||||
docker run cli-builder make -B vendor compose-jsonschema
|
||||
|
||||
- store_artifacts:
|
||||
path: /work/build
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
|
||||
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
|
||||
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointCreate(ctx context.Context, container string, options types.CheckpointCreateOptions) error {
|
||||
if cli.checkpointCreateFunc != nil {
|
||||
return cli.checkpointCreateFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointDelete(ctx context.Context, container string, options types.CheckpointDeleteOptions) error {
|
||||
if cli.checkpointDeleteFunc != nil {
|
||||
return cli.checkpointDeleteFunc(container, options)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointList(ctx context.Context, container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
if cli.checkpointListFunc != nil {
|
||||
return cli.checkpointListFunc(container, options)
|
||||
}
|
||||
return []types.Checkpoint{}, nil
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckpointCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointCreateFunc func(container string, options types.CheckpointCreateOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
|
||||
return errors.Errorf("error creating checkpoint for container foo")
|
||||
},
|
||||
expectedError: "error creating checkpoint for container foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: tc.checkpointCreateFunc,
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
var exit bool
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options types.CheckpointCreateOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
exit = options.Exit
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
checkpoint := "checkpoint-bar"
|
||||
cmd.SetArgs([]string{"container-foo", checkpoint})
|
||||
cmd.Flags().Set("leave-running", "true")
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "container-foo", containerID)
|
||||
assert.Equal(t, checkpoint, checkpointID)
|
||||
assert.Equal(t, "/dir/foo", checkpointDir)
|
||||
assert.Equal(t, false, exit)
|
||||
assert.Equal(t, checkpoint, strings.TrimSpace(cli.OutBuffer().String()))
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckpointListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointListFunc func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
return []types.Checkpoint{}, errors.Errorf("error getting checkpoints for container foo")
|
||||
},
|
||||
expectedError: "error getting checkpoints for container foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
checkpointListFunc: tc.checkpointListFunc,
|
||||
}, &bytes.Buffer{})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointListWithOptions(t *testing.T) {
|
||||
var containerID, checkpointDir string
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return []types.Checkpoint{
|
||||
{Name: "checkpoint-foo"},
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo"})
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "container-foo", containerID)
|
||||
assert.Equal(t, "/dir/foo", checkpointDir)
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "checkpoint-list-with-options.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointDeleteFunc func(container string, options types.CheckpointDeleteOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
args: []string{"too-few-arguments"},
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||
return errors.Errorf("error deleting checkpoint")
|
||||
},
|
||||
expectedError: "error deleting checkpoint",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
checkpointDeleteFunc: tc.checkpointDeleteFunc,
|
||||
}, &bytes.Buffer{})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
return nil
|
||||
},
|
||||
}, &bytes.Buffer{})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "container-foo", containerID)
|
||||
assert.Equal(t, "checkpoint-bar", checkpointID)
|
||||
assert.Equal(t, "/dir/foo", checkpointDir)
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
CHECKPOINT NAME
|
||||
checkpoint-foo
|
||||
@ -1,19 +1,21 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
@ -38,7 +40,7 @@ type Cli interface {
|
||||
In() *InStream
|
||||
SetIn(in *InStream)
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
CredentialsStore(serverAddress string) credentials.Store
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
@ -103,10 +105,59 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
return cli.server
|
||||
}
|
||||
|
||||
// GetAllCredentials returns all of the credentials stored in all of the
|
||||
// configured credential stores.
|
||||
func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) {
|
||||
auths := make(map[string]types.AuthConfig)
|
||||
for registry := range cli.configFile.CredentialHelpers {
|
||||
helper := cli.CredentialsStore(registry)
|
||||
newAuths, err := helper.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAll(auths, newAuths)
|
||||
}
|
||||
defaultStore := cli.CredentialsStore("")
|
||||
newAuths, err := defaultStore.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addAll(auths, newAuths)
|
||||
return auths, nil
|
||||
}
|
||||
|
||||
func addAll(to, from map[string]types.AuthConfig) {
|
||||
for reg, ac := range from {
|
||||
to[reg] = ac
|
||||
}
|
||||
}
|
||||
|
||||
// CredentialsStore returns a new credentials store based
|
||||
// on the settings provided in the configuration file. Empty string returns
|
||||
// the default credential store.
|
||||
func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store {
|
||||
if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" {
|
||||
return credentials.NewNativeStore(cli.configFile, helper)
|
||||
}
|
||||
return credentials.NewFileStore(cli.configFile)
|
||||
}
|
||||
|
||||
// getConfiguredCredentialStore returns the credential helper configured for the
|
||||
// given registry, the default credsStore, or the empty string if neither are
|
||||
// configured.
|
||||
func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string {
|
||||
if c.CredentialHelpers != nil && serverAddress != "" {
|
||||
if helper, exists := c.CredentialHelpers[serverAddress]; exists {
|
||||
return helper
|
||||
}
|
||||
}
|
||||
return c.CredentialsStore
|
||||
}
|
||||
|
||||
// Initialize the dockerCli runs initialization that must happen after command
|
||||
// line flags are parsed.
|
||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
|
||||
cli.configFile = LoadDefaultConfigFile(cli.err)
|
||||
|
||||
var err error
|
||||
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||
@ -142,10 +193,15 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||
OSType: ping.OSType,
|
||||
}
|
||||
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
} else {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.server = ServerInfo{HasExperimental: true}
|
||||
// since the new header was added in 1.25, assume server is 1.24 if header is not present.
|
||||
if ping.APIVersion == "" {
|
||||
ping.APIVersion = "1.24"
|
||||
}
|
||||
|
||||
// if server version is lower than the current cli, downgrade
|
||||
if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) {
|
||||
cli.client.UpdateClientVersion(ping.APIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -163,6 +219,19 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
|
||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
|
||||
}
|
||||
|
||||
// LoadDefaultConfigFile attempts to load the default config file and returns
|
||||
// an initialized ConfigFile struct if none is found.
|
||||
func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile {
|
||||
configFile, e := cliconfig.Load(cliconfig.Dir())
|
||||
if e != nil {
|
||||
fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e)
|
||||
}
|
||||
if !configFile.ContainsAuth() {
|
||||
credentials.DetectDefaultStore(configFile)
|
||||
}
|
||||
return configFile
|
||||
}
|
||||
|
||||
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
|
||||
@ -216,10 +285,6 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
DialContext: (&net.Dialer{
|
||||
KeepAlive: 30 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
}).DialContext,
|
||||
}
|
||||
proto, addr, _, err := client.ParseHost(host)
|
||||
if err != nil {
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/system"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -64,7 +65,7 @@ func runConfigCreate(dockerCli command.Cli, options createOptions) error {
|
||||
spec := swarm.ConfigSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: options.name,
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
},
|
||||
Data: configData,
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@ -40,10 +41,11 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: tc.configCreateFunc,
|
||||
}),
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
@ -53,6 +55,7 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
|
||||
func TestConfigCreateWithName(t *testing.T) {
|
||||
name := "foo"
|
||||
buf := new(bytes.Buffer)
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
@ -66,14 +69,14 @@ func TestConfigCreateWithName(t *testing.T) {
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
expected := golden.Get(t, actual, configDataFile)
|
||||
assert.Equal(t, string(expected), string(actual))
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||
}
|
||||
|
||||
func TestConfigCreateWithLabels(t *testing.T) {
|
||||
@ -83,6 +86,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
}
|
||||
name := "foo"
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
@ -97,12 +101,12 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
|
||||
cmd := newConfigCreateCommand(cli)
|
||||
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
|
||||
cmd.Flags().Set("label", "lbl1=Label-foo")
|
||||
cmd.Flags().Set("label", "lbl2=Label-bar")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
@ -97,7 +97,7 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
@ -137,7 +137,7 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
@ -174,7 +174,7 @@ func TestConfigInspectPretty(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"configID"})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
@ -35,10 +36,11 @@ func TestConfigListErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigListCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configListFunc: tc.configListFunc,
|
||||
}),
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
@ -47,6 +49,7 @@ func TestConfigListErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
@ -64,16 +67,18 @@ func TestConfigList(t *testing.T) {
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.SetOutput(cli.OutBuffer())
|
||||
cmd.SetOutput(buf)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
@ -83,16 +88,18 @@ func TestConfigListWithQuietOption(t *testing.T) {
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-quiet-option.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
@ -102,18 +109,19 @@ func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{
|
||||
ConfigFormat: "{{ .Name }} {{ .Labels }}",
|
||||
})
|
||||
cmd := newConfigListCommand(cli)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-config-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
@ -123,16 +131,17 @@ func TestConfigListWithFormat(t *testing.T) {
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-format.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
func TestConfigListWithFilter(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(options types.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Equal(t, "foo", options.Filters.Get("name")[0])
|
||||
@ -152,12 +161,13 @@ func TestConfigListWithFilter(t *testing.T) {
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("filter", "name=foo")
|
||||
cmd.Flags().Set("filter", "label=lbl1=Label-bar")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigRemoveCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: tc.configRemoveFunc,
|
||||
}, buf),
|
||||
)
|
||||
@ -47,7 +47,7 @@ func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
buf := new(bytes.Buffer)
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return nil
|
||||
@ -65,7 +65,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
@ -77,7 +77,6 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.EqualError(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Equal(t, names, removedConfigs)
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"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/spf13/cobra"
|
||||
@ -23,26 +22,8 @@ type attachOptions struct {
|
||||
container string
|
||||
}
|
||||
|
||||
func inspectContainerAndCheckState(ctx context.Context, cli client.APIClient, args string) (*types.ContainerJSON, error) {
|
||||
c, err := cli.ContainerInspect(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.State.Running {
|
||||
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts attachOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -62,15 +43,23 @@ func NewAttachCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error {
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, client, opts.container)
|
||||
c, err := client.ContainerInspect(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !c.State.Running {
|
||||
return errors.New("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
|
||||
if c.State.Paused {
|
||||
return errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
|
||||
if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -106,19 +95,6 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
// If use docker attach command to attach to a stop container, it will return
|
||||
// "You cannot attach to a stopped container" error, it's ok, but when
|
||||
// attach to a running container, it(docker attach) use inspect to check
|
||||
// the container's state, if it pass the state check on the client side,
|
||||
// and then the container is stopped, docker attach command still attach to
|
||||
// the container and not exit.
|
||||
//
|
||||
// Recheck the container's state to avoid attach block.
|
||||
_, err = inspectContainerAndCheckState(ctx, client, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestNewAttachCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{Running: false}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
c := types.ContainerJSON{}
|
||||
c.ContainerJSONBase = &types.ContainerJSONBase{}
|
||||
c.ContainerJSONBase.State = &types.ContainerState{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
Restarting: true,
|
||||
}
|
||||
return c, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewAttachCommand(test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
containerInspectFunc func(string) (types.ContainerJSON, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
if cli.containerInspectFunc != nil {
|
||||
return cli.containerInspectFunc(containerID)
|
||||
}
|
||||
return types.ContainerJSON{}, nil
|
||||
}
|
||||
@ -33,7 +33,7 @@ func newExecOptions() *execOptions {
|
||||
}
|
||||
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
options := newExecOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -63,7 +63,7 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func runExec(dockerCli command.Cli, options *execOptions, container string, execCmd []string) error {
|
||||
func runExec(dockerCli *command.DockerCli, options *execOptions, container string, execCmd []string) error {
|
||||
execConfig, err := parseExec(options, execCmd)
|
||||
// just in case the ParseExec does not exit
|
||||
if container == "" || err != nil {
|
||||
@ -111,7 +111,12 @@ func runExec(dockerCli command.Cli, options *execOptions, container string, exec
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
|
||||
return client.ContainerExecStart(ctx, execID, execStartCheck)
|
||||
if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil {
|
||||
return err
|
||||
}
|
||||
// For now don't print this - wait for when we support exec wait()
|
||||
// fmt.Fprintf(dockerCli.Out(), "%s\n", execID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interactive exec requested.
|
||||
|
||||
@ -1,14 +1,9 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type arguments struct {
|
||||
@ -78,7 +73,9 @@ func TestParseExec(t *testing.T) {
|
||||
|
||||
for valid, expectedExecConfig := range valids {
|
||||
execConfig, err := parseExec(&valid.options, valid.execCmd)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !compareExecConfig(expectedExecConfig, execConfig) {
|
||||
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
|
||||
}
|
||||
@ -117,28 +114,3 @@ func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) boo
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestNewExecCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (types.ContainerJSON, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.Errorf("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc})
|
||||
cmd := NewExecCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +110,7 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
|
||||
|
||||
func (h *hijackedIOStreamer) beginOutputStream(restoreInput func()) <-chan error {
|
||||
if h.outputStream == nil && h.errorStream == nil {
|
||||
// There is no need to copy output.
|
||||
// Ther is no need to copy output.
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ func (h *hijackedIOStreamer) beginInputStream(restoreInput func()) (doneC <-chan
|
||||
if err != nil {
|
||||
// This error will also occur on the receive
|
||||
// side (from stdout) where it will be
|
||||
// propagated back to the caller.
|
||||
// propogated back to the caller.
|
||||
logrus.Debugf("Error sendStdin: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,19 +12,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/strslice"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var (
|
||||
deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
|
||||
deviceCgroupRuleRegexp = regexp.MustCompile("^[acb] ([0-9]+|\\*):([0-9]+|\\*) [rwm]{1,3}$")
|
||||
)
|
||||
|
||||
// containerOptions is a data object with all the options for creating a container
|
||||
@ -333,8 +333,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
volumes := copts.volumes.GetMap()
|
||||
// add any bind targets to the list of container volumes
|
||||
for bind := range copts.volumes.GetMap() {
|
||||
parsed, _ := loader.ParseVolume(bind)
|
||||
if parsed.Source != "" {
|
||||
if arr := volumeSplitN(bind, 2); len(arr) > 1 {
|
||||
// after creating the bind mount we want to delete it from the copts.volumes values because
|
||||
// we do not want bind mounts being committed to image configs
|
||||
binds = append(binds, bind)
|
||||
@ -410,13 +409,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
}
|
||||
|
||||
// collect all the environment variables for the container
|
||||
envVariables, err := opts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
|
||||
envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// collect all the labels for the container
|
||||
labels, err := opts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
|
||||
labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -441,7 +440,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
return nil, errors.Errorf("--userns: invalid USER mode")
|
||||
}
|
||||
|
||||
restartPolicy, err := opts.ParseRestartPolicy(copts.restartPolicy)
|
||||
restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -554,7 +553,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
MacAddress: copts.macAddress,
|
||||
Entrypoint: entrypoint,
|
||||
WorkingDir: copts.workingDir,
|
||||
Labels: opts.ConvertKVStringsToMap(labels),
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(labels),
|
||||
Healthcheck: healthConfig,
|
||||
}
|
||||
if flags.Changed("stop-signal") {
|
||||
@ -667,7 +666,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
}
|
||||
|
||||
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
|
||||
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
|
||||
loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts)
|
||||
if loggingDriver == "none" && len(loggingOpts) > 0 {
|
||||
return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver)
|
||||
}
|
||||
@ -829,6 +828,67 @@ func validatePath(val string, validator func(string) bool) (string, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// volumeSplitN splits raw into a maximum of n parts, separated by a separator colon.
|
||||
// A separator colon is the last `:` character in the regex `[:\\]?[a-zA-Z]:` (note `\\` is `\` escaped).
|
||||
// In Windows driver letter appears in two situations:
|
||||
// a. `^[a-zA-Z]:` (A colon followed by `^[a-zA-Z]:` is OK as colon is the separator in volume option)
|
||||
// b. A string in the format like `\\?\C:\Windows\...` (UNC).
|
||||
// Therefore, a driver letter can only follow either a `:` or `\\`
|
||||
// This allows to correctly split strings such as `C:\foo:D:\:rw` or `/tmp/q:/foo`.
|
||||
func volumeSplitN(raw string, n int) []string {
|
||||
var array []string
|
||||
if len(raw) == 0 || raw[0] == ':' {
|
||||
// invalid
|
||||
return nil
|
||||
}
|
||||
// numberOfParts counts the number of parts separated by a separator colon
|
||||
numberOfParts := 0
|
||||
// left represents the left-most cursor in raw, updated at every `:` character considered as a separator.
|
||||
left := 0
|
||||
// right represents the right-most cursor in raw incremented with the loop. Note this
|
||||
// starts at index 1 as index 0 is already handle above as a special case.
|
||||
for right := 1; right < len(raw); right++ {
|
||||
// stop parsing if reached maximum number of parts
|
||||
if n >= 0 && numberOfParts >= n {
|
||||
break
|
||||
}
|
||||
if raw[right] != ':' {
|
||||
continue
|
||||
}
|
||||
potentialDriveLetter := raw[right-1]
|
||||
if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') {
|
||||
if right > 1 {
|
||||
beforePotentialDriveLetter := raw[right-2]
|
||||
// Only `:` or `\\` are checked (`/` could fall into the case of `/tmp/q:/foo`)
|
||||
if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '\\' {
|
||||
// e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`.
|
||||
array = append(array, raw[left:right])
|
||||
left = right + 1
|
||||
numberOfParts++
|
||||
}
|
||||
// else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing.
|
||||
}
|
||||
// if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing.
|
||||
} else {
|
||||
// if `:` is not preceded by a potential drive letter, then consider it as a delimiter.
|
||||
array = append(array, raw[left:right])
|
||||
left = right + 1
|
||||
numberOfParts++
|
||||
}
|
||||
}
|
||||
// need to take care of the last part
|
||||
if left < len(raw) {
|
||||
if n >= 0 && numberOfParts >= n {
|
||||
// if the maximum number of parts is reached, just append the rest to the last part
|
||||
// left-1 is at the last `:` that needs to be included since not considered a separator.
|
||||
array[n-1] += raw[left-1:]
|
||||
} else {
|
||||
array = append(array, raw[left:])
|
||||
}
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
// validateAttach validates that the specified string is a valid attach option.
|
||||
func validateAttach(val string) (string, error) {
|
||||
s := strings.ToLower(val)
|
||||
|
||||
@ -19,7 +19,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateAttach(t *testing.T) {
|
||||
@ -62,14 +61,16 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
|
||||
return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err
|
||||
}
|
||||
|
||||
func parseMustError(t *testing.T, args string) {
|
||||
_, _, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
|
||||
assert.Error(t, err, args)
|
||||
func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) {
|
||||
config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " "))
|
||||
return config, hostConfig, err
|
||||
}
|
||||
|
||||
func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) {
|
||||
config, hostConfig, _, err := parseRun(append(strings.Split(args, " "), "ubuntu", "bash"))
|
||||
assert.NoError(t, err)
|
||||
config, hostConfig, err := parsetest(t, args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return config, hostConfig
|
||||
}
|
||||
|
||||
@ -85,6 +86,7 @@ func TestParseRunLinks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func TestParseRunAttach(t *testing.T) {
|
||||
if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr {
|
||||
t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
|
||||
@ -101,17 +103,31 @@ func TestParseRunAttach(t *testing.T) {
|
||||
if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr {
|
||||
t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRunWithInvalidArgs(t *testing.T) {
|
||||
parseMustError(t, "-a")
|
||||
parseMustError(t, "-a invalid")
|
||||
parseMustError(t, "-a invalid -a stdout")
|
||||
parseMustError(t, "-a stdout -a stderr -d")
|
||||
parseMustError(t, "-a stdin -d")
|
||||
parseMustError(t, "-a stdout -d")
|
||||
parseMustError(t, "-a stderr -d")
|
||||
parseMustError(t, "-d --rm")
|
||||
if _, _, err := parsetest(t, "-a"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-a invalid"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a invalid` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-a stdin -d"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a stdin -d` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-a stdout -d"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a stdout -d` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-a stderr -d"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-a stderr -d` should be an error but is not")
|
||||
}
|
||||
if _, _, err := parsetest(t, "-d --rm"); err == nil {
|
||||
t.Fatal("Error parsing attach flags, `-d --rm` should be an error but is not")
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
@ -367,46 +383,51 @@ 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")
|
||||
|
||||
if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" {
|
||||
t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err)
|
||||
}
|
||||
// ipc ok
|
||||
_, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"})
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hostconfig.IpcMode.Valid() {
|
||||
t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
|
||||
}
|
||||
|
||||
// pid ko
|
||||
_, _, _, err = parseRun([]string{"--pid=container:", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, "--pid: invalid PID mode")
|
||||
|
||||
if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" {
|
||||
t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err)
|
||||
}
|
||||
// pid ok
|
||||
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hostconfig.PidMode.Valid() {
|
||||
t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode)
|
||||
}
|
||||
|
||||
// uts ko
|
||||
_, _, _, err = parseRun([]string{"--uts=container:", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, "--uts: invalid UTS mode")
|
||||
|
||||
if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" {
|
||||
t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err)
|
||||
}
|
||||
// uts ok
|
||||
_, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"})
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hostconfig.UTSMode.Valid() {
|
||||
t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode)
|
||||
}
|
||||
|
||||
// shm-size ko
|
||||
expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'`
|
||||
_, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, expectedErr)
|
||||
|
||||
if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr {
|
||||
t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err)
|
||||
}
|
||||
// shm-size ok
|
||||
_, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"})
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hostconfig.ShmSize != 134217728 {
|
||||
t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
|
||||
}
|
||||
@ -750,6 +771,58 @@ func callDecodeContainerConfig(volumes []string, binds []string) (*container.Con
|
||||
return c, h, err
|
||||
}
|
||||
|
||||
func TestVolumeSplitN(t *testing.T) {
|
||||
for _, x := range []struct {
|
||||
input string
|
||||
n int
|
||||
expected []string
|
||||
}{
|
||||
{`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}},
|
||||
{`:C:\foo:d:`, -1, nil},
|
||||
{`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}},
|
||||
{`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}},
|
||||
{`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}},
|
||||
|
||||
{`d:\`, -1, []string{`d:\`}},
|
||||
{`d:`, -1, []string{`d:`}},
|
||||
{`d:\path`, -1, []string{`d:\path`}},
|
||||
{`d:\path with space`, -1, []string{`d:\path with space`}},
|
||||
{`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}},
|
||||
{`c:\:d:\`, -1, []string{`c:\`, `d:\`}},
|
||||
{`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}},
|
||||
{`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}},
|
||||
{`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}},
|
||||
{`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}},
|
||||
{`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}},
|
||||
{`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}},
|
||||
{`name:D:`, -1, []string{`name`, `D:`}},
|
||||
{`name:D::rW`, -1, []string{`name`, `D:`, `rW`}},
|
||||
{`name:D::RW`, -1, []string{`name`, `D:`, `RW`}},
|
||||
{`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}},
|
||||
{`c:\Windows`, -1, []string{`c:\Windows`}},
|
||||
{`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}},
|
||||
|
||||
{``, -1, nil},
|
||||
{`.`, -1, []string{`.`}},
|
||||
{`..\`, -1, []string{`..\`}},
|
||||
{`c:\:..\`, -1, []string{`c:\`, `..\`}},
|
||||
{`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}},
|
||||
|
||||
// Cover directories with one-character name
|
||||
{`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}},
|
||||
} {
|
||||
res := volumeSplitN(x.input, x.n)
|
||||
if len(res) < len(x.expected) {
|
||||
t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
|
||||
}
|
||||
for i, e := range res {
|
||||
if e != x.expected[i] {
|
||||
t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDevice(t *testing.T) {
|
||||
valid := []string{
|
||||
"/home",
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
@ -13,12 +12,12 @@ import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/libnetwork/resolvconf/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -78,43 +77,21 @@ func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
// they are trying to set a DNS to a localhost address
|
||||
func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if isLocalhost(dnsIP) {
|
||||
if dns.IsLocalhost(dnsIP) {
|
||||
fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPLocalhost is a regex pattern for IPv4 or IPv6 loopback range.
|
||||
const ipLocalhost = `((127\.([0-9]{1,3}\.){2}[0-9]{1,3})|(::1)$)`
|
||||
|
||||
var localhostIPRegexp = regexp.MustCompile(ipLocalhost)
|
||||
|
||||
// IsLocalhost returns true if ip matches the localhost IP regular expression.
|
||||
// Used for determining if nameserver settings are being passed which are
|
||||
// localhost addresses
|
||||
func isLocalhost(ip string) bool {
|
||||
return localhostIPRegexp.MatchString(ip)
|
||||
}
|
||||
|
||||
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, ropts *runOptions, copts *containerOptions) error {
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), copts.env.GetAll())
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
newEnv = append(newEnv, k)
|
||||
} else {
|
||||
newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, *v))
|
||||
}
|
||||
}
|
||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error {
|
||||
containerConfig, err := parse(flags, copts)
|
||||
// just in case the parse does not exit
|
||||
if err != nil {
|
||||
reportError(dockerCli.Err(), "run", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
return runContainer(dockerCli, ropts, copts, containerConfig)
|
||||
return runContainer(dockerCli, opts, copts, containerConfig)
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
@ -170,7 +147,6 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain
|
||||
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
var (
|
||||
waitDisplayID chan struct{}
|
||||
errCh chan error
|
||||
@ -190,11 +166,10 @@ func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *contain
|
||||
}
|
||||
|
||||
close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID)
|
||||
|
||||
defer close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
}
|
||||
|
||||
statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove)
|
||||
|
||||
@ -146,12 +146,12 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
|
||||
fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
}
|
||||
}
|
||||
if attachErr := <-cErr; attachErr != nil {
|
||||
if attchErr := <-cErr; attchErr != nil {
|
||||
if _, ok := err.(term.EscapeError); ok {
|
||||
// The user entered the detach escape sequence.
|
||||
return nil
|
||||
}
|
||||
return attachErr
|
||||
return attchErr
|
||||
}
|
||||
|
||||
if status := <-statusChan; status != 0 {
|
||||
|
||||
@ -106,7 +106,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
closeChan <- err
|
||||
}
|
||||
for _, container := range cs {
|
||||
s := formatter.NewContainerStats(container.ID[:12])
|
||||
s := formatter.NewContainerStats(container.ID[:12], daemonOSType)
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
@ -123,7 +123,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
eh := command.InitEventHandler()
|
||||
eh.Handle("create", func(e events.Message) {
|
||||
if opts.all {
|
||||
s := formatter.NewContainerStats(e.ID[:12])
|
||||
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
@ -132,7 +132,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
})
|
||||
|
||||
eh.Handle("start", func(e events.Message) {
|
||||
s := formatter.NewContainerStats(e.ID[:12])
|
||||
s := formatter.NewContainerStats(e.ID[:12], daemonOSType)
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
@ -158,7 +158,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
// Artificially send creation events for the containers we were asked to
|
||||
// monitor (same code path than we use when monitoring all containers).
|
||||
for _, name := range opts.containers {
|
||||
s := formatter.NewContainerStats(name)
|
||||
s := formatter.NewContainerStats(name, daemonOSType)
|
||||
if cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst)
|
||||
|
||||
@ -16,8 +16,9 @@ import (
|
||||
)
|
||||
|
||||
type stats struct {
|
||||
mu sync.Mutex
|
||||
cs []*formatter.ContainerStats
|
||||
ostype string
|
||||
mu sync.Mutex
|
||||
cs []*formatter.ContainerStats
|
||||
}
|
||||
|
||||
// daemonOSType is set once we have at least one stat for a container
|
||||
|
||||
@ -39,7 +39,7 @@ func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id strin
|
||||
}
|
||||
|
||||
// MonitorTtySize updates the container tty size when the terminal tty changes size
|
||||
func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool) error {
|
||||
func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isExec bool) error {
|
||||
resizeTty := func() {
|
||||
height, width := cli.Out().GetTtySize()
|
||||
resizeTtyTo(ctx, cli.Client(), id, height, width, isExec)
|
||||
@ -74,7 +74,7 @@ func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool
|
||||
}
|
||||
|
||||
// ForwardAllSignals forwards signals to the container
|
||||
func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string) chan os.Signal {
|
||||
func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal {
|
||||
sigc := make(chan os.Signal, 128)
|
||||
signal.CatchAll(sigc)
|
||||
go func() {
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
containertypes "github.com/docker/docker/api/types/container"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -81,7 +82,7 @@ func runUpdate(dockerCli *command.DockerCli, options *updateOptions) error {
|
||||
|
||||
var restartPolicy containertypes.RestartPolicy
|
||||
if options.restartPolicy != "" {
|
||||
restartPolicy, err = opts.ParseRestartPolicy(options.restartPolicy)
|
||||
restartPolicy, err = runconfigopts.ParseRestartPolicy(options.restartPolicy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli,
|
||||
|
||||
// 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) {
|
||||
func getExitCode(ctx context.Context, dockerCli *command.DockerCli, 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.
|
||||
|
||||
@ -2,16 +2,16 @@ package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
"github.com/docker/go-units"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -171,16 +171,16 @@ func (c *containerContext) Command() string {
|
||||
}
|
||||
|
||||
func (c *containerContext) CreatedAt() string {
|
||||
return time.Unix(c.c.Created, 0).String()
|
||||
return time.Unix(int64(c.c.Created), 0).String()
|
||||
}
|
||||
|
||||
func (c *containerContext) RunningFor() string {
|
||||
createdAt := time.Unix(c.c.Created, 0)
|
||||
createdAt := time.Unix(int64(c.c.Created), 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *containerContext) Ports() string {
|
||||
return DisplayablePorts(c.c.Ports)
|
||||
return api.DisplayablePorts(c.c.Ports)
|
||||
}
|
||||
|
||||
func (c *containerContext) Status() string {
|
||||
@ -257,89 +257,3 @@ func (c *containerContext) Networks() string {
|
||||
|
||||
return strings.Join(networks, ",")
|
||||
}
|
||||
|
||||
// DisplayablePorts returns formatted string representing open ports of container
|
||||
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
|
||||
// it's used by command 'docker ps'
|
||||
func DisplayablePorts(ports []types.Port) string {
|
||||
type portGroup struct {
|
||||
first uint16
|
||||
last uint16
|
||||
}
|
||||
groupMap := make(map[string]*portGroup)
|
||||
var result []string
|
||||
var hostMappings []string
|
||||
var groupMapKeys []string
|
||||
sort.Sort(byPortInfo(ports))
|
||||
for _, port := range ports {
|
||||
current := port.PrivatePort
|
||||
portKey := port.Type
|
||||
if port.IP != "" {
|
||||
if port.PublicPort != current {
|
||||
hostMappings = append(hostMappings, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
|
||||
continue
|
||||
}
|
||||
portKey = fmt.Sprintf("%s/%s", port.IP, port.Type)
|
||||
}
|
||||
group := groupMap[portKey]
|
||||
|
||||
if group == nil {
|
||||
groupMap[portKey] = &portGroup{first: current, last: current}
|
||||
// record order that groupMap keys are created
|
||||
groupMapKeys = append(groupMapKeys, portKey)
|
||||
continue
|
||||
}
|
||||
if current == (group.last + 1) {
|
||||
group.last = current
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, formGroup(portKey, group.first, group.last))
|
||||
groupMap[portKey] = &portGroup{first: current, last: current}
|
||||
}
|
||||
for _, portKey := range groupMapKeys {
|
||||
g := groupMap[portKey]
|
||||
result = append(result, formGroup(portKey, g.first, g.last))
|
||||
}
|
||||
result = append(result, hostMappings...)
|
||||
return strings.Join(result, ", ")
|
||||
}
|
||||
|
||||
func formGroup(key string, start, last uint16) string {
|
||||
parts := strings.Split(key, "/")
|
||||
groupType := parts[0]
|
||||
var ip string
|
||||
if len(parts) > 1 {
|
||||
ip = parts[0]
|
||||
groupType = parts[1]
|
||||
}
|
||||
group := strconv.Itoa(int(start))
|
||||
if start != last {
|
||||
group = fmt.Sprintf("%s-%d", group, last)
|
||||
}
|
||||
if ip != "" {
|
||||
group = fmt.Sprintf("%s:%s->%s", ip, group, group)
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", group, groupType)
|
||||
}
|
||||
|
||||
// byPortInfo is a temporary type used to sort types.Port by its fields
|
||||
type byPortInfo []types.Port
|
||||
|
||||
func (r byPortInfo) Len() int { return len(r) }
|
||||
func (r byPortInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r byPortInfo) Less(i, j int) bool {
|
||||
if r[i].PrivatePort != r[j].PrivatePort {
|
||||
return r[i].PrivatePort < r[j].PrivatePort
|
||||
}
|
||||
|
||||
if r[i].IP != r[j].IP {
|
||||
return r[i].IP < r[j].IP
|
||||
}
|
||||
|
||||
if r[i].PublicPort != r[j].PublicPort {
|
||||
return r[i].PublicPort < r[j].PublicPort
|
||||
}
|
||||
|
||||
return r[i].Type < r[j].Type
|
||||
}
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContainerPsContext(t *testing.T) {
|
||||
@ -227,7 +226,7 @@ size: 0B
|
||||
Context{Format: NewContainerFormat("{{.Image}}", false, true)},
|
||||
"ubuntu\nubuntu\n",
|
||||
},
|
||||
// Special headers for customized table format
|
||||
// Special headers for customerized 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
|
||||
@ -358,11 +357,12 @@ func TestContainerContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, expectedJSONs[i], m)
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,11 +377,12 @@ func TestContainerContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var s string
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, containers[i].ID, s, msg)
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, containers[i].ID, s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -410,248 +411,3 @@ func TestContainerBackCompat(t *testing.T) {
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
type ports struct {
|
||||
ports []types.Port
|
||||
expected string
|
||||
}
|
||||
|
||||
// nolint: lll
|
||||
func TestDisplayablePorts(t *testing.T) {
|
||||
cases := []ports{
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"9988/tcp"},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9988/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "0.0.0.0",
|
||||
PrivatePort: 9988,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"0.0.0.0:0->9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"4.3.2.1:8899->9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 9988,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"4.3.2.1:9988->9988/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
Type: "udp",
|
||||
}, {
|
||||
PrivatePort: 9988,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9988/udp, 9988/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 9998,
|
||||
PrivatePort: 9998,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 9999,
|
||||
PrivatePort: 9999,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"1.2.3.4:9998-9999->9998-9999/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 8887,
|
||||
PrivatePort: 9998,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PublicPort: 8888,
|
||||
PrivatePort: 9999,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"1.2.3.4:8887->9998/udp, 1.2.3.4:8888->9999/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9998,
|
||||
Type: "udp",
|
||||
}, {
|
||||
PrivatePort: 9999,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9998-9999/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 6677,
|
||||
PublicPort: 7766,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"9988/udp, 1.2.3.4:7766->6677/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 2233,
|
||||
PublicPort: 3322,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"4.3.2.1:3322->2233/tcp, 1.2.3.4:8899->9988/tcp, 1.2.3.4:8899->9988/udp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 9988,
|
||||
PublicPort: 8899,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.2.3.4",
|
||||
PrivatePort: 6677,
|
||||
PublicPort: 7766,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "4.3.2.1",
|
||||
PrivatePort: 2233,
|
||||
PublicPort: 3322,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
"9988/udp, 4.3.2.1:3322->2233/tcp, 1.2.3.4:7766->6677/tcp",
|
||||
},
|
||||
{
|
||||
[]types.Port{
|
||||
{
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
PrivatePort: 1024,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
PrivatePort: 80,
|
||||
Type: "udp",
|
||||
}, {
|
||||
PrivatePort: 1024,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "1.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 80,
|
||||
PrivatePort: 1024,
|
||||
Type: "udp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: "2.1.1.1",
|
||||
PublicPort: 1024,
|
||||
PrivatePort: 80,
|
||||
Type: "udp",
|
||||
},
|
||||
},
|
||||
"80/tcp, 80/udp, 1024/tcp, 1024/udp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, port := range cases {
|
||||
actual := DisplayablePorts(port.ports)
|
||||
assert.Equal(t, port.expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,12 +29,11 @@ const (
|
||||
// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
|
||||
type DiskUsageContext struct {
|
||||
Context
|
||||
Verbose bool
|
||||
LayersSize int64
|
||||
Images []*types.ImageSummary
|
||||
Containers []*types.Container
|
||||
Volumes []*types.Volume
|
||||
BuilderSize int64
|
||||
Verbose bool
|
||||
LayersSize int64
|
||||
Images []*types.ImageSummary
|
||||
Containers []*types.Container
|
||||
Volumes []*types.Volume
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) {
|
||||
@ -66,59 +65,49 @@ reclaimable: {{.Reclaimable}}
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) Write() (err error) {
|
||||
if ctx.Verbose {
|
||||
return ctx.verboseWrite()
|
||||
}
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preFormat()
|
||||
if ctx.Verbose == false {
|
||||
ctx.buffer = bytes.NewBufferString("")
|
||||
ctx.preFormat()
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
|
||||
totalSize: ctx.LayersSize,
|
||||
images: ctx.Images,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
|
||||
containers: ctx.Containers,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
|
||||
volumes: ctx.Volumes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
|
||||
diskUsageContainersCtx.header = map[string]string{
|
||||
"Type": typeHeader,
|
||||
"TotalCount": totalHeader,
|
||||
"Active": activeHeader,
|
||||
"Size": sizeHeader,
|
||||
"Reclaimable": reclaimableHeader,
|
||||
}
|
||||
ctx.postFormat(tmpl, &diskUsageContainersCtx)
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.contextFormat(tmpl, &diskUsageImagesContext{
|
||||
totalSize: ctx.LayersSize,
|
||||
images: ctx.Images,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ctx.contextFormat(tmpl, &diskUsageContainersContext{
|
||||
containers: ctx.Containers,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{
|
||||
volumes: ctx.Volumes,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ctx.contextFormat(tmpl, &diskUsageBuilderContext{
|
||||
builderSize: ctx.BuilderSize,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}}
|
||||
diskUsageContainersCtx.header = map[string]string{
|
||||
"Type": typeHeader,
|
||||
"TotalCount": totalHeader,
|
||||
"Active": activeHeader,
|
||||
"Size": sizeHeader,
|
||||
"Reclaimable": reclaimableHeader,
|
||||
}
|
||||
ctx.postFormat(tmpl, &diskUsageContainersCtx)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) verboseWrite() (err error) {
|
||||
// First images
|
||||
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
|
||||
if err != nil {
|
||||
@ -187,9 +176,6 @@ func (ctx *DiskUsageContext) verboseWrite() (err error) {
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newVolumeContext())
|
||||
|
||||
// And build cache
|
||||
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
|
||||
return
|
||||
}
|
||||
|
||||
@ -243,11 +229,12 @@ func (c *diskUsageImagesContext) Reclaimable() string {
|
||||
if c.totalSize > 0 {
|
||||
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize)
|
||||
}
|
||||
return units.HumanSize(float64(reclaimable))
|
||||
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
|
||||
}
|
||||
|
||||
type diskUsageContainersContext struct {
|
||||
HeaderContext
|
||||
verbose bool
|
||||
containers []*types.Container
|
||||
}
|
||||
|
||||
@ -305,11 +292,12 @@ func (c *diskUsageContainersContext) Reclaimable() string {
|
||||
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
|
||||
}
|
||||
|
||||
return units.HumanSize(float64(reclaimable))
|
||||
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
|
||||
}
|
||||
|
||||
type diskUsageVolumesContext struct {
|
||||
HeaderContext
|
||||
verbose bool
|
||||
volumes []*types.Volume
|
||||
}
|
||||
|
||||
@ -366,34 +354,5 @@ func (c *diskUsageVolumesContext) Reclaimable() string {
|
||||
return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize)
|
||||
}
|
||||
|
||||
return units.HumanSize(float64(reclaimable))
|
||||
}
|
||||
|
||||
type diskUsageBuilderContext struct {
|
||||
HeaderContext
|
||||
builderSize int64
|
||||
}
|
||||
|
||||
func (c *diskUsageBuilderContext) MarshalJSON() ([]byte, error) {
|
||||
return marshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *diskUsageBuilderContext) Type() string {
|
||||
return "Build Cache"
|
||||
}
|
||||
|
||||
func (c *diskUsageBuilderContext) TotalCount() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *diskUsageBuilderContext) Active() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *diskUsageBuilderContext) Size() string {
|
||||
return units.HumanSize(float64(c.builderSize))
|
||||
}
|
||||
|
||||
func (c *diskUsageBuilderContext) Reclaimable() string {
|
||||
return c.Size()
|
||||
return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable)))
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ func TestDiskUsageContextFormatWrite(t *testing.T) {
|
||||
Images 0 0 0B 0B
|
||||
Containers 0 0 0B 0B
|
||||
Local Volumes 0 0 0B 0B
|
||||
Build Cache 0B 0B
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -39,9 +38,6 @@ CONTAINER ID IMAGE COMMAND LOCAL VOLUMES
|
||||
Local Volumes space usage:
|
||||
|
||||
VOLUME NAME LINKS SIZE
|
||||
|
||||
Build cache usage: 0B
|
||||
|
||||
`,
|
||||
},
|
||||
// Errors
|
||||
@ -74,7 +70,6 @@ Build cache usage: 0B
|
||||
Images 0 0 0B 0B
|
||||
Containers 0 0 0B 0B
|
||||
Local Volumes 0 0 0B 0B
|
||||
Build Cache 0B 0B
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -87,7 +82,6 @@ Build Cache 0B
|
||||
Images 0
|
||||
Containers 0
|
||||
Local Volumes 0
|
||||
Build Cache
|
||||
`,
|
||||
},
|
||||
// Raw Format
|
||||
@ -115,12 +109,6 @@ active: 0
|
||||
size: 0B
|
||||
reclaimable: 0B
|
||||
|
||||
type: Build Cache
|
||||
total:
|
||||
active:
|
||||
size: 0B
|
||||
reclaimable: 0B
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
@ -79,18 +79,21 @@ func (c *historyContext) ID() string {
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
|
||||
var created string
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
|
||||
return created
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedSince() string {
|
||||
created := units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
|
||||
var created string
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
|
||||
return created + " ago"
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedBy() string {
|
||||
createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1)
|
||||
if c.trunc {
|
||||
return stringutils.Ellipsis(createdBy, 45)
|
||||
createdBy = stringutils.Ellipsis(createdBy, 45)
|
||||
}
|
||||
return createdBy
|
||||
}
|
||||
|
||||
@ -234,12 +234,12 @@ func (c *imageContext) Digest() string {
|
||||
}
|
||||
|
||||
func (c *imageContext) CreatedSince() string {
|
||||
createdAt := time.Unix(c.i.Created, 0)
|
||||
createdAt := time.Unix(int64(c.i.Created), 0)
|
||||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *imageContext) CreatedAt() string {
|
||||
return time.Unix(c.i.Created, 0).String()
|
||||
return time.Unix(int64(c.i.Created), 0).String()
|
||||
}
|
||||
|
||||
func (c *imageContext) Size() string {
|
||||
|
||||
@ -3,7 +3,6 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -11,7 +10,6 @@ import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNetworkContext(t *testing.T) {
|
||||
@ -185,11 +183,12 @@ func TestNetworkContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, expectedJSONs[i], m)
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,10 +203,11 @@ func TestNetworkContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var s string
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, networks[i].ID, s, msg)
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, networks[i].ID, s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -11,7 +10,6 @@ import (
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNodeContext(t *testing.T) {
|
||||
@ -250,11 +248,12 @@ func TestNodeContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, testcase.expected[i], m, msg)
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, testcase.expected[i], m)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -270,11 +269,12 @@ func TestNodeContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var s string
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, nodes[i].ID, s, msg)
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, nodes[i].ID, s)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ func (d *dummy) Func1() string {
|
||||
return "Func1"
|
||||
}
|
||||
|
||||
func (d *dummy) func2() string { // nolint: unused
|
||||
func (d *dummy) func2() string {
|
||||
return "func2(should not be marshalled)"
|
||||
}
|
||||
|
||||
|
||||
@ -3,13 +3,11 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServiceContextWrite(t *testing.T) {
|
||||
@ -202,11 +200,12 @@ func TestServiceContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, expectedJSONs[i], m)
|
||||
}
|
||||
}
|
||||
func TestServiceContextWriteJSONField(t *testing.T) {
|
||||
@ -230,10 +229,11 @@ func TestServiceContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var s string
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, services[i].Spec.Name, s, msg)
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, services[i].Spec.Name, s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,8 +109,10 @@ func NewStatsFormat(source, osType string) Format {
|
||||
}
|
||||
|
||||
// NewContainerStats returns a new ContainerStats entity and sets in it the given name
|
||||
func NewContainerStats(container string) *ContainerStats {
|
||||
return &ContainerStats{StatsEntry: StatsEntry{Container: container}}
|
||||
func NewContainerStats(container, osType string) *ContainerStats {
|
||||
return &ContainerStats{
|
||||
StatsEntry: StatsEntry{Container: container},
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerStatsWrite renders the context for a list of containers statistics
|
||||
@ -184,7 +186,7 @@ func (c *containerStatsContext) MemUsage() string {
|
||||
return fmt.Sprintf("-- / --")
|
||||
}
|
||||
if c.os == winOSType {
|
||||
return units.BytesSize(c.s.Memory)
|
||||
return fmt.Sprintf("%s", units.BytesSize(c.s.Memory))
|
||||
}
|
||||
return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit))
|
||||
}
|
||||
|
||||
@ -3,14 +3,12 @@ package formatter
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestVolumeContext(t *testing.T) {
|
||||
@ -155,11 +153,12 @@ func TestVolumeContextWriteJSON(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var m map[string]interface{}
|
||||
err := json.Unmarshal([]byte(line), &m)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, expectedJSONs[i], m, msg)
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, expectedJSONs[i], m)
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,10 +173,11 @@ func TestVolumeContextWriteJSONField(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
msg := fmt.Sprintf("Output: line %d: %s", i, line)
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var s string
|
||||
err := json.Unmarshal([]byte(line), &s)
|
||||
require.NoError(t, err, msg)
|
||||
assert.Equal(t, volumes[i].Name, s, msg)
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, volumes[i].Name, s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
@ -26,6 +25,7 @@ import (
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/urlutil"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
units "github.com/docker/go-units"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@ -62,7 +62,6 @@ type buildOptions struct {
|
||||
squash bool
|
||||
target string
|
||||
imageIDFile string
|
||||
stream bool
|
||||
}
|
||||
|
||||
// dockerfileFromStdin returns true when the user specified that the Dockerfile
|
||||
@ -77,20 +76,16 @@ func (o buildOptions) contextFromStdin() bool {
|
||||
return o.context == "-"
|
||||
}
|
||||
|
||||
func newBuildOptions() buildOptions {
|
||||
// NewBuildCommand creates a new `docker build` command
|
||||
func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
ulimits := make(map[string]*units.Ulimit)
|
||||
return buildOptions{
|
||||
options := buildOptions{
|
||||
tags: opts.NewListOpts(validateTag),
|
||||
buildArgs: opts.NewListOpts(opts.ValidateEnv),
|
||||
ulimits: opts.NewUlimitOpt(&ulimits),
|
||||
labels: opts.NewListOpts(opts.ValidateEnv),
|
||||
extraHosts: opts.NewListOpts(opts.ValidateExtraHost),
|
||||
}
|
||||
}
|
||||
|
||||
// NewBuildCommand creates a new `docker build` command
|
||||
func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options := newBuildOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "build [OPTIONS] PATH | URL | -",
|
||||
@ -139,10 +134,6 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.SetAnnotation("squash", "experimental", nil)
|
||||
flags.SetAnnotation("squash", "version", []string{"1.25"})
|
||||
|
||||
flags.BoolVar(&options.stream, "stream", false, "Stream attaches to server to negotiate build context")
|
||||
flags.SetAnnotation("stream", "experimental", nil)
|
||||
flags.SetAnnotation("stream", "version", []string{"1.31"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -163,7 +154,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
|
||||
var (
|
||||
buildCtx io.ReadCloser
|
||||
dockerfileCtx io.ReadCloser
|
||||
@ -173,7 +164,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
relDockerfile string
|
||||
progBuff io.Writer
|
||||
buildBuff io.Writer
|
||||
remote string
|
||||
)
|
||||
|
||||
if options.dockerfileFromStdin() {
|
||||
@ -199,7 +189,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
|
||||
switch {
|
||||
case options.contextFromStdin():
|
||||
// buildCtx is tar archive. if stdin was dockerfile then it is wrapped
|
||||
buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
|
||||
case isLocalDir(specifiedContext):
|
||||
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
|
||||
@ -223,8 +212,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
contextDir = tempDir
|
||||
}
|
||||
|
||||
// read from a directory into tar archive
|
||||
if buildCtx == nil && !options.stream {
|
||||
if buildCtx == nil {
|
||||
excludes, err := build.ReadDockerignore(contextDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -241,7 +229,13 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
}
|
||||
|
||||
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
|
||||
|
||||
compression := archive.Uncompressed
|
||||
if options.compress {
|
||||
compression = archive.Gzip
|
||||
}
|
||||
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
|
||||
Compression: compression,
|
||||
ExcludePatterns: excludes,
|
||||
})
|
||||
if err != nil {
|
||||
@ -249,52 +243,24 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
// replace Dockerfile if it was added from stdin and there is archive context
|
||||
if dockerfileCtx != nil && buildCtx != nil {
|
||||
// replace Dockerfile if added dynamically
|
||||
if dockerfileCtx != nil {
|
||||
buildCtx, relDockerfile, err = build.AddDockerfileToBuildContext(dockerfileCtx, buildCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if streaming and dockerfile was not from stdin then read from file
|
||||
// to the same reader that is usually stdin
|
||||
if options.stream && dockerfileCtx == nil {
|
||||
dockerfileCtx, err = os.Open(relDockerfile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to open %s", relDockerfile)
|
||||
}
|
||||
defer dockerfileCtx.Close()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
ctx := context.Background()
|
||||
|
||||
var resolvedTags []*resolvedTag
|
||||
if command.IsTrusted() {
|
||||
translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
|
||||
return TrustedReference(ctx, dockerCli, ref, nil)
|
||||
}
|
||||
// if there is a tar wrapper, the dockerfile needs to be replaced inside it
|
||||
if buildCtx != nil {
|
||||
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
|
||||
// Dockerfile which uses trusted pulls.
|
||||
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
|
||||
} else if dockerfileCtx != nil {
|
||||
// if there was not archive context still do the possible replacements in Dockerfile
|
||||
newDockerfile, _, err := rewriteDockerfileFrom(ctx, dockerfileCtx, translator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dockerfileCtx = ioutil.NopCloser(bytes.NewBuffer(newDockerfile))
|
||||
}
|
||||
}
|
||||
|
||||
if options.compress {
|
||||
buildCtx, err = build.Compress(buildCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
|
||||
// Dockerfile which uses trusted pulls.
|
||||
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
|
||||
}
|
||||
|
||||
// Setup an upload progress bar
|
||||
@ -303,46 +269,9 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
progressOutput = &lastProgressOutput{output: progressOutput}
|
||||
}
|
||||
|
||||
// if up to this point nothing has set the context then we must have have
|
||||
// another way for sending it(streaming) and set the context to the Dockerfile
|
||||
if dockerfileCtx != nil && buildCtx == nil {
|
||||
buildCtx = dockerfileCtx
|
||||
}
|
||||
var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
|
||||
|
||||
s, err := trySession(dockerCli, contextDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if buildCtx != nil && !options.stream {
|
||||
body = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
|
||||
}
|
||||
|
||||
// add context stream to the session
|
||||
if options.stream && s != nil {
|
||||
syncDone := make(chan error) // used to signal first progress reporting completed.
|
||||
// progress would also send errors but don't need it here as errors
|
||||
// are handled by session.Run() and ImageBuild()
|
||||
if err := addDirToSession(s, contextDir, progressOutput, syncDone); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := newBufferedWriter(syncDone, buildBuff)
|
||||
defer func() {
|
||||
select {
|
||||
case <-buf.flushed:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
buildBuff = buf
|
||||
|
||||
remote = clientSessionRemote
|
||||
body = buildCtx
|
||||
}
|
||||
|
||||
configFile := dockerCli.ConfigFile()
|
||||
authConfigs, _ := configFile.GetAllCredentials()
|
||||
authConfigs, _ := dockerCli.GetAllCredentials()
|
||||
buildOptions := types.ImageBuildOptions{
|
||||
Memory: options.memory.Value(),
|
||||
MemorySwap: options.memorySwap.Value(),
|
||||
@ -362,27 +291,15 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
Dockerfile: relDockerfile,
|
||||
ShmSize: options.shmSize.Value(),
|
||||
Ulimits: options.ulimits.GetList(),
|
||||
BuildArgs: configFile.ParseProxyConfig(dockerCli.Client().DaemonHost(), options.buildArgs.GetAll()),
|
||||
BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()),
|
||||
AuthConfigs: authConfigs,
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
CacheFrom: options.cacheFrom,
|
||||
SecurityOpt: options.securityOpt,
|
||||
NetworkMode: options.networkMode,
|
||||
Squash: options.squash,
|
||||
ExtraHosts: options.extraHosts.GetAll(),
|
||||
Target: options.target,
|
||||
RemoteContext: remote,
|
||||
}
|
||||
|
||||
if s != nil {
|
||||
go func() {
|
||||
logrus.Debugf("running session: %v", s.UUID())
|
||||
if err := s.Run(ctx, dockerCli.Client().DialSession); err != nil {
|
||||
logrus.Error(err)
|
||||
cancel() // cancel progress context
|
||||
}
|
||||
}()
|
||||
buildOptions.SessionID = s.UUID()
|
||||
}
|
||||
|
||||
response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions)
|
||||
@ -390,7 +307,6 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
|
||||
if options.quiet {
|
||||
fmt.Fprintf(dockerCli.Err(), "%s", progBuff)
|
||||
}
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
package build
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/builder/remotecontext/git"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/docker/pkg/fileutils"
|
||||
"github.com/docker/docker/pkg/gitutils"
|
||||
"github.com/docker/docker/pkg/httputils"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/pools"
|
||||
"github.com/docker/docker/pkg/progress"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
@ -29,8 +29,6 @@ import (
|
||||
const (
|
||||
// DefaultDockerfileName is the Default filename with Docker commands, read by docker build
|
||||
DefaultDockerfileName string = "Dockerfile"
|
||||
// archiveHeaderSize is the number of bytes in an archive header
|
||||
archiveHeaderSize = 512
|
||||
)
|
||||
|
||||
// ValidateContextDirectory checks if all the contents of the directory
|
||||
@ -87,12 +85,12 @@ func ValidateContextDirectory(srcPath string, excludes []string) error {
|
||||
func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) {
|
||||
buf := bufio.NewReader(r)
|
||||
|
||||
magic, err := buf.Peek(archiveHeaderSize)
|
||||
magic, err := buf.Peek(archive.HeaderSize)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err)
|
||||
}
|
||||
|
||||
if IsArchive(magic) {
|
||||
if archive.IsArchive(magic) {
|
||||
return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil
|
||||
}
|
||||
|
||||
@ -136,18 +134,6 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl
|
||||
|
||||
}
|
||||
|
||||
// IsArchive checks for the magic bytes of a tar or any supported compression
|
||||
// algorithm.
|
||||
func IsArchive(header []byte) bool {
|
||||
compression := archive.DetectCompression(header)
|
||||
if compression != archive.Uncompressed {
|
||||
return true
|
||||
}
|
||||
r := tar.NewReader(bytes.NewBuffer(header))
|
||||
_, err := r.Next()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetContextFromGitURL uses a Git URL as context for a `docker build`. The
|
||||
// git repo is cloned into a temporary directory used as the context directory.
|
||||
// Returns the absolute path to the temporary context directory, the relative
|
||||
@ -157,7 +143,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
return "", "", errors.Wrapf(err, "unable to find 'git'")
|
||||
}
|
||||
absContextDir, err := git.Clone(gitURL)
|
||||
absContextDir, err := gitutils.Clone(gitURL)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrapf(err, "unable to 'git clone' to temporary context directory")
|
||||
}
|
||||
@ -175,7 +161,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error)
|
||||
// Returns the tar archive used for the context and a path of the
|
||||
// dockerfile inside the tar.
|
||||
func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) {
|
||||
response, err := getWithStatusError(remoteURL)
|
||||
response, err := httputils.Download(remoteURL)
|
||||
if err != nil {
|
||||
return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err)
|
||||
}
|
||||
@ -187,24 +173,6 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read
|
||||
return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
|
||||
}
|
||||
|
||||
// getWithStatusError does an http.Get() and returns an error if the
|
||||
// status code is 4xx or 5xx.
|
||||
func getWithStatusError(url string) (resp *http.Response, err error) {
|
||||
if resp, err = http.Get(url); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode < 400 {
|
||||
return resp, nil
|
||||
}
|
||||
msg := fmt.Sprintf("failed to GET %s with status %s", url, resp.Status)
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, msg+": error reading body")
|
||||
}
|
||||
return nil, errors.Errorf(msg+": %s", bytes.TrimSpace(body))
|
||||
}
|
||||
|
||||
// GetContextFromLocalDir uses the given local directory as context for a
|
||||
// `docker build`. Returns the absolute path to the local context directory,
|
||||
// the relative path of the dockerfile in that context directory, and a non-nil
|
||||
@ -376,27 +344,3 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
|
||||
})
|
||||
return buildCtx, randomName, nil
|
||||
}
|
||||
|
||||
// Compress the build context for sending to the API
|
||||
func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
go func() {
|
||||
compressWriter, err := archive.CompressStream(pipeWriter, archive.Gzip)
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
}
|
||||
defer buildCtx.Close()
|
||||
|
||||
if _, err := pools.Copy(compressWriter, buildCtx); err != nil {
|
||||
pipeWriter.CloseWithError(
|
||||
errors.Wrap(err, "failed to compress context"))
|
||||
compressWriter.Close()
|
||||
return
|
||||
}
|
||||
compressWriter.Close()
|
||||
pipeWriter.Close()
|
||||
}()
|
||||
|
||||
return pipeReader, nil
|
||||
}
|
||||
|
||||
@ -266,35 +266,3 @@ func chdir(t *testing.T, dir string) func() {
|
||||
require.NoError(t, os.Chdir(dir))
|
||||
return func() { require.NoError(t, os.Chdir(workingDirectory)) }
|
||||
}
|
||||
|
||||
func TestIsArchive(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
header []byte
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
doc: "nil is not a valid header",
|
||||
header: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
doc: "invalid header bytes",
|
||||
header: []byte{0x00, 0x01, 0x02},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
doc: "header for bzip2 archive",
|
||||
header: []byte{0x42, 0x5A, 0x68},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
doc: "header for 7zip archive is not supported",
|
||||
header: []byte{0x50, 0x4b, 0x03, 0x04},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
assert.Equal(t, testcase.expected, IsArchive(testcase.header), testcase.doc)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,151 +0,0 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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/pkg/errors"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const clientSessionRemote = "client-session"
|
||||
|
||||
func isSessionSupported(dockerCli command.Cli) bool {
|
||||
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
|
||||
}
|
||||
|
||||
func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) {
|
||||
var s *session.Session
|
||||
if isSessionSupported(dockerCli) {
|
||||
sharedKey, err := getBuildSharedKey(contextDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get build shared key")
|
||||
}
|
||||
s, err = session.NewSession(filepath.Base(contextDir), sharedKey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create session")
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func addDirToSession(session *session.Session, contextDir string, progressOutput progress.Output, done chan error) error {
|
||||
excludes, err := build.ReadDockerignore(contextDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p := &sizeProgress{out: progressOutput, action: "Streaming build context to Docker daemon"}
|
||||
|
||||
workdirProvider := filesync.NewFSSyncProvider(contextDir, excludes)
|
||||
session.Allow(workdirProvider)
|
||||
|
||||
// this will be replaced on parallel build jobs. keep the current
|
||||
// progressbar for now
|
||||
if snpc, ok := workdirProvider.(interface {
|
||||
SetNextProgressCallback(func(int, bool), chan error)
|
||||
}); ok {
|
||||
snpc.SetNextProgressCallback(p.update, done)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type sizeProgress struct {
|
||||
out progress.Output
|
||||
action string
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (sp *sizeProgress) update(size int, last bool) {
|
||||
if sp.limiter == nil {
|
||||
sp.limiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
|
||||
}
|
||||
if last || sp.limiter.Allow() {
|
||||
sp.out.WriteProgress(progress.Progress{Action: sp.action, Current: int64(size), LastUpdate: last})
|
||||
}
|
||||
}
|
||||
|
||||
type bufferedWriter struct {
|
||||
done chan error
|
||||
io.Writer
|
||||
buf *bytes.Buffer
|
||||
flushed chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newBufferedWriter(done chan error, w io.Writer) *bufferedWriter {
|
||||
bw := &bufferedWriter{done: done, Writer: w, buf: new(bytes.Buffer), flushed: make(chan struct{})}
|
||||
go func() {
|
||||
<-done
|
||||
bw.flushBuffer()
|
||||
}()
|
||||
return bw
|
||||
}
|
||||
|
||||
func (bw *bufferedWriter) Write(dt []byte) (int, error) {
|
||||
select {
|
||||
case <-bw.done:
|
||||
bw.flushBuffer()
|
||||
return bw.Writer.Write(dt)
|
||||
default:
|
||||
return bw.buf.Write(dt)
|
||||
}
|
||||
}
|
||||
|
||||
func (bw *bufferedWriter) flushBuffer() {
|
||||
bw.mu.Lock()
|
||||
select {
|
||||
case <-bw.flushed:
|
||||
default:
|
||||
bw.Writer.Write(bw.buf.Bytes())
|
||||
close(bw.flushed)
|
||||
}
|
||||
bw.mu.Unlock()
|
||||
}
|
||||
|
||||
func getBuildSharedKey(dir string) (string, error) {
|
||||
// build session is hash of build dir with node based randomness
|
||||
s := sha256.Sum256([]byte(fmt.Sprintf("%s:%s", tryNodeIdentifier(), dir)))
|
||||
return hex.EncodeToString(s[:]), nil
|
||||
}
|
||||
|
||||
func tryNodeIdentifier() (out string) {
|
||||
out = cliconfig.Dir() // return config dir as default on permission error
|
||||
if err := os.MkdirAll(cliconfig.Dir(), 0700); err == nil {
|
||||
sessionFile := filepath.Join(cliconfig.Dir(), ".buildNodeID")
|
||||
if _, err := os.Lstat(sessionFile); err != nil {
|
||||
if os.IsNotExist(err) { // create a new file with stored randomness
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return
|
||||
}
|
||||
if err := ioutil.WriteFile(sessionFile, []byte(hex.EncodeToString(b)), 0600); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dt, err := ioutil.ReadFile(sessionFile)
|
||||
if err == nil {
|
||||
return string(dt)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
|
||||
dest, err := ioutil.TempDir("", "test-build-compress-dest")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dest)
|
||||
|
||||
var dockerfileName string
|
||||
fakeImageBuild := func(_ context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
tee := io.TeeReader(context, buffer)
|
||||
|
||||
assert.NoError(t, archive.Untar(tee, dest, nil))
|
||||
dockerfileName = options.Dockerfile
|
||||
|
||||
header := buffer.Bytes()[:10]
|
||||
assert.Equal(t, archive.Gzip, archive.DetectCompression(header))
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
return types.ImageBuildResponse{Body: ioutil.NopCloser(body)}, nil
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
|
||||
dockerfile := bytes.NewBufferString(`
|
||||
FROM alpine:3.6
|
||||
COPY foo /
|
||||
`)
|
||||
cli.SetIn(command.NewInStream(ioutil.NopCloser(dockerfile)))
|
||||
|
||||
dir, err := ioutil.TempDir("", "test-build-compress")
|
||||
require.NoError(t, err)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
ioutil.WriteFile(filepath.Join(dir, "foo"), []byte("some content"), 0644)
|
||||
|
||||
options := newBuildOptions()
|
||||
options.compress = true
|
||||
options.dockerfileName = "-"
|
||||
options.context = dir
|
||||
|
||||
err = runBuild(cli, options)
|
||||
require.NoError(t, err)
|
||||
|
||||
files, err := ioutil.ReadDir(dest)
|
||||
require.NoError(t, err)
|
||||
actual := []string{}
|
||||
for _, fileInfo := range files {
|
||||
actual = append(actual, fileInfo.Name())
|
||||
}
|
||||
sort.Strings(actual)
|
||||
assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual)
|
||||
}
|
||||
@ -27,7 +27,6 @@ type fakeClient struct {
|
||||
imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
|
||||
imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
|
||||
imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error)
|
||||
imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error {
|
||||
@ -115,10 +114,3 @@ func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.Hist
|
||||
}
|
||||
return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) ImageBuild(ctx context.Context, context io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
|
||||
if cli.imageBuildFunc != nil {
|
||||
return cli.imageBuildFunc(ctx, context, options)
|
||||
}
|
||||
return types.ImageBuildResponse{Body: ioutil.NopCloser(strings.NewReader(""))}, nil
|
||||
}
|
||||
|
||||
@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
// NewImageCommand returns a cobra command for `image` subcommands
|
||||
func NewImageCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// nolint: interfacer
|
||||
func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "image",
|
||||
Short: "Manage images",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
@ -37,7 +38,8 @@ func TestNewHistoryCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}))
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -88,13 +90,13 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc})
|
||||
cmd := NewHistoryCommand(cli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
if tc.outputRegex == "" {
|
||||
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:])
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
|
||||
|
||||
@ -37,7 +37,7 @@ func TestNewImportCommandErrors(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewImportCommand(test.NewFakeCliWithOutput(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -45,7 +45,7 @@ func TestNewImportCommandErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewImportCommandInvalidFile(t *testing.T) {
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"})
|
||||
testutil.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file")
|
||||
@ -92,7 +92,7 @@ func TestNewImportCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewImportCommand(test.NewFakeCliWithOutput(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
|
||||
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
@ -27,7 +27,7 @@ func TestNewInspectCommandErrors(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newInspectCommand(test.NewFakeCliWithOutput(&fakeClient{}, buf))
|
||||
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -79,7 +79,7 @@ func TestNewInspectCommandSuccess(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
imageInspectInvocationCount = 0
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newInspectCommand(test.NewFakeCliWithOutput(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf))
|
||||
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -36,7 +36,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}))
|
||||
cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, new(bytes.Buffer)))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -81,8 +81,8 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{imageListFunc: tc.imageListFunc}, buf)
|
||||
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
|
||||
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
|
||||
cmd := NewImagesCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
@ -95,7 +95,7 @@ func TestNewImagesCommandSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNewListCommandAlias(t *testing.T) {
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd := newListCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
|
||||
assert.True(t, cmd.HasAlias("images"))
|
||||
assert.True(t, cmd.HasAlias("list"))
|
||||
assert.False(t, cmd.HasAlias("other"))
|
||||
|
||||
@ -43,7 +43,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
|
||||
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, new(bytes.Buffer))
|
||||
cli.In().SetIsTerminal(tc.isTerminalIn)
|
||||
cmd := NewLoadCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
@ -54,7 +54,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
|
||||
|
||||
func TestNewLoadCommandInvalidInput(t *testing.T) {
|
||||
expectedError := "open *"
|
||||
cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs([]string{"--input", "*"})
|
||||
err := cmd.Execute()
|
||||
@ -93,7 +93,7 @@ func TestNewLoadCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewLoadCommand(test.NewFakeCliWithOutput(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf))
|
||||
cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
|
||||
@ -38,7 +38,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewPruneCommand(test.NewFakeCliWithOutput(&fakeClient{
|
||||
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
|
||||
imagesPruneFunc: tc.imagesPruneFunc,
|
||||
}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
@ -86,7 +86,7 @@ func TestNewPruneCommandSuccess(t *testing.T) {
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewPruneCommand(test.NewFakeCliWithOutput(&fakeClient{
|
||||
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
|
||||
imagesPruneFunc: tc.imagesPruneFunc,
|
||||
}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
|
||||
@ -1,21 +1,29 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNewPullCommandErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named,
|
||||
authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error
|
||||
}{
|
||||
{
|
||||
name: "wrong-args",
|
||||
@ -39,8 +47,8 @@ func TestNewPullCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := NewPullCommand(cli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -49,8 +57,10 @@ func TestNewPullCommandErrors(t *testing.T) {
|
||||
|
||||
func TestNewPullCommandSuccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
name string
|
||||
args []string
|
||||
trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named,
|
||||
authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
@ -62,13 +72,13 @@ func TestNewPullCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cmd := NewPullCommand(cli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
actual := cli.OutBuffer().String()
|
||||
actual := buf.String()
|
||||
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:])
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
|
||||
}
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNewPushCommandErrors(t *testing.T) {
|
||||
@ -45,8 +50,8 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc})
|
||||
cmd := NewPushCommand(cli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -55,8 +60,11 @@ func TestNewPushCommandErrors(t *testing.T) {
|
||||
|
||||
func TestNewPushCommandSuccess(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
name string
|
||||
args []string
|
||||
trustedPushFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo,
|
||||
ref reference.Named, authConfig types.AuthConfig,
|
||||
requestPrivilege types.RequestPrivilegeFunc) error
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
@ -64,12 +72,12 @@ func TestNewPushCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewPushCommand(test.NewFakeCli(&fakeClient{
|
||||
imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
})
|
||||
cmd := NewPushCommand(cli)
|
||||
}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
@ -56,8 +56,8 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
}
|
||||
|
||||
var errs []string
|
||||
for _, img := range images {
|
||||
dels, err := client.ImageRemove(ctx, img, options)
|
||||
for _, image := range images {
|
||||
dels, err := client.ImageRemove(ctx, image, options)
|
||||
if err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
@ -72,11 +72,7 @@ func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
msg := strings.Join(errs, "\n")
|
||||
if !opts.force {
|
||||
return errors.New(msg)
|
||||
}
|
||||
fmt.Fprintf(dockerCli.Err(), msg)
|
||||
return errors.Errorf("%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
@ -14,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNewRemoveCommandAlias(t *testing.T) {
|
||||
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
|
||||
assert.True(t, cmd.HasAlias("rmi"))
|
||||
assert.True(t, cmd.HasAlias("remove"))
|
||||
assert.False(t, cmd.HasAlias("other"))
|
||||
@ -45,7 +46,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageRemoveFunc: tc.imageRemoveFunc,
|
||||
}))
|
||||
}, new(bytes.Buffer)))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -57,7 +58,6 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
|
||||
expectedErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "Image Deleted",
|
||||
@ -67,15 +67,6 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
return []types.ImageDeleteResponseItem{{Deleted: image}}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Image Deleted with force option",
|
||||
args: []string{"-f", "image1"},
|
||||
imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
|
||||
assert.Equal(t, "image1", image)
|
||||
return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
|
||||
},
|
||||
expectedErrMsg: "error removing image",
|
||||
},
|
||||
{
|
||||
name: "Image Untagged",
|
||||
args: []string{"image1"},
|
||||
@ -96,15 +87,16 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
fakeCli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
|
||||
cmd := NewRemoveCommand(fakeCli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageRemoveFunc: tc.imageRemoveFunc,
|
||||
}, buf))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
if tc.expectedErrMsg != "" {
|
||||
assert.Equal(t, tc.expectedErrMsg, fakeCli.ErrBuffer().String())
|
||||
}
|
||||
actual := fakeCli.OutBuffer().String()
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
actual := buf.String()
|
||||
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:])
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ func TestNewSaveCommandErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
|
||||
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}, new(bytes.Buffer))
|
||||
cli.Out().SetIsTerminal(tc.isTerminal)
|
||||
cmd := NewSaveCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
@ -85,7 +85,7 @@ func TestNewSaveCommandSuccess(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cmd := NewSaveCommand(test.NewFakeCliWithOutput(&fakeClient{
|
||||
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
|
||||
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
@ -16,8 +17,9 @@ func TestCliNewTagCommandErrors(t *testing.T) {
|
||||
{"image1", "image2", "image3"},
|
||||
}
|
||||
expectedError := "\"tag\" requires exactly 2 argument(s)."
|
||||
buf := new(bytes.Buffer)
|
||||
for _, args := range testCases {
|
||||
cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}))
|
||||
cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}, buf))
|
||||
cmd.SetArgs(args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), expectedError)
|
||||
@ -25,6 +27,7 @@ func TestCliNewTagCommandErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCliNewTagCommand(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := NewTagCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
imageTagFunc: func(image string, ref string) error {
|
||||
@ -32,7 +35,7 @@ func TestCliNewTagCommand(t *testing.T) {
|
||||
assert.Equal(t, "image2", ref)
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"image1", "image2"})
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
|
||||
@ -21,9 +21,6 @@
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": ""
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -48,9 +45,6 @@
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": ""
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -21,9 +21,6 @@
|
||||
},
|
||||
"RootFS": {
|
||||
"Type": ""
|
||||
},
|
||||
"Metadata": {
|
||||
"LastTagTime": "0001-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
Untagged: image1
|
||||
Deleted: image2
|
||||
Untagged: image1
|
||||
Deleted: image2
|
||||
|
||||
@ -1 +1,2 @@
|
||||
Deleted: image1
|
||||
Deleted: image1
|
||||
|
||||
@ -1 +1,2 @@
|
||||
Untagged: image1
|
||||
Untagged: image1
|
||||
|
||||
@ -110,7 +110,6 @@ func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error {
|
||||
buffer := new(bytes.Buffer)
|
||||
rdr := bytes.NewReader(rawElement)
|
||||
dec := json.NewDecoder(rdr)
|
||||
dec.UseNumber()
|
||||
|
||||
if rawErr := dec.Decode(&raw); rawErr != nil {
|
||||
return errors.Errorf("unable to read inspect data: %v", rawErr)
|
||||
|
||||
@ -6,8 +6,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/templates"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testElement struct {
|
||||
@ -221,39 +219,3 @@ func TestIndentedInspectorRawElements(t *testing.T) {
|
||||
t.Fatalf("Expected `%s`, got `%s`", expected, b.String())
|
||||
}
|
||||
}
|
||||
|
||||
// moby/moby#32235
|
||||
// This test verifies that even if `tryRawInspectFallback` is called the fields containing
|
||||
// numerical values are displayed correctly.
|
||||
// For example, `docker inspect --format "{{.Id}} {{.Size}} alpine` and
|
||||
// `docker inspect --format "{{.ID}} {{.Size}} alpine" will have the same output which is
|
||||
// sha256:651aa95985aa4a17a38ffcf71f598ec461924ca96865facc2c5782ef2d2be07f 3983636
|
||||
func TestTemplateInspectorRawFallbackNumber(t *testing.T) {
|
||||
// Using typedElem to automatically fall to tryRawInspectFallback.
|
||||
typedElem := struct {
|
||||
ID string `json:"Id"`
|
||||
}{"ad3"}
|
||||
testcases := []struct {
|
||||
raw []byte
|
||||
exp string
|
||||
}{
|
||||
{raw: []byte(`{"Id": "ad3", "Size": 53317}`), exp: "53317 ad3\n"},
|
||||
{raw: []byte(`{"Id": "ad3", "Size": 53317.102}`), exp: "53317.102 ad3\n"},
|
||||
{raw: []byte(`{"Id": "ad3", "Size": 53317.0}`), exp: "53317.0 ad3\n"},
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
tmpl, err := templates.Parse("{{.Size}} {{.Id}}")
|
||||
require.NoError(t, err)
|
||||
|
||||
i := NewTemplateInspector(b, tmpl)
|
||||
for _, tc := range testcases {
|
||||
err = i.Inspect(typedElem, tc.raw)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = i.Flush()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.exp, b.String())
|
||||
b.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||
if c.networkCreateFunc != nil {
|
||||
return c.networkCreateFunc(ctx, name, options)
|
||||
}
|
||||
return types.NetworkCreateResponse{}, nil
|
||||
}
|
||||
@ -8,7 +8,8 @@ import (
|
||||
)
|
||||
|
||||
// NewNetworkCommand returns a cobra command for `network` subcommands
|
||||
func NewNetworkCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// nolint: interfacer
|
||||
func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "network",
|
||||
Short: "Manage networks",
|
||||
|
||||
@ -19,7 +19,7 @@ type connectOptions struct {
|
||||
linklocalips []string
|
||||
}
|
||||
|
||||
func newConnectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newConnectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
options := connectOptions{
|
||||
links: opts.NewListOpts(opts.ValidateLink),
|
||||
}
|
||||
@ -45,7 +45,7 @@ func newConnectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runConnect(dockerCli command.Cli, options connectOptions) error {
|
||||
func runConnect(dockerCli *command.DockerCli, options connectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
epConfig := &network.EndpointSettings{
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
@ -36,7 +37,7 @@ type createOptions struct {
|
||||
ipamOpt opts.MapOpts
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
options := createOptions{
|
||||
driverOpts: *opts.NewMapOpts(nil, nil),
|
||||
labels: opts.NewListOpts(opts.ValidateEnv),
|
||||
@ -82,7 +83,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(dockerCli command.Cli, options createOptions) error {
|
||||
func runCreate(dockerCli *command.DockerCli, options createOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
ipamCfg, err := consolidateIpam(options.ipamSubnet, options.ipamIPRange, options.ipamGateway, options.ipamAux.GetAll())
|
||||
@ -106,7 +107,7 @@ func runCreate(dockerCli command.Cli, options createOptions) error {
|
||||
Ingress: options.ingress,
|
||||
Scope: options.scope,
|
||||
ConfigOnly: options.configOnly,
|
||||
Labels: opts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()),
|
||||
}
|
||||
|
||||
if from := options.configFrom; from != "" {
|
||||
@ -232,13 +233,13 @@ func subnetMatches(subnet, data string) (bool, error) {
|
||||
|
||||
_, s, err := net.ParseCIDR(subnet)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "invalid subnet")
|
||||
return false, errors.Errorf("Invalid subnet %s : %v", s, err)
|
||||
}
|
||||
|
||||
if strings.Contains(data, "/") {
|
||||
ip, _, err = net.ParseCIDR(data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, errors.Errorf("Invalid cidr %s : %v", data, err)
|
||||
}
|
||||
} else {
|
||||
ip = net.ParseIP(data)
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNetworkCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
expectedError: "exactly 1 argument",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
networkCreateFunc: func(ctx context.Context, name string, createBody types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||
return types.NetworkCreateResponse{}, errors.Errorf("error creating network")
|
||||
},
|
||||
expectedError: "error creating network",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "255.255.0.0/24",
|
||||
"gateway": "255.0.255.0/24",
|
||||
"subnet": "10.1.2.0.30.50",
|
||||
},
|
||||
expectedError: "invalid CIDR address: 10.1.2.0.30.50",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "255.255.0.0.30/24",
|
||||
"gateway": "255.0.255.0/24",
|
||||
"subnet": "255.0.0.0/24",
|
||||
},
|
||||
expectedError: "invalid CIDR address: 255.255.0.0.30/24",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"gateway": "255.0.0.0/24",
|
||||
},
|
||||
expectedError: "every ip-range or gateway must have a corresponding subnet",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "255.0.0.0/24",
|
||||
},
|
||||
expectedError: "every ip-range or gateway must have a corresponding subnet",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "255.0.0.0/24",
|
||||
"gateway": "255.0.0.0/24",
|
||||
},
|
||||
expectedError: "every ip-range or gateway must have a corresponding subnet",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "255.255.0.0/24",
|
||||
"gateway": "255.0.255.0/24",
|
||||
"subnet": "10.1.2.0/23,10.1.3.248/30",
|
||||
},
|
||||
expectedError: "multiple overlapping subnet configuration is not supported",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "192.168.1.0/24,192.168.1.200/24",
|
||||
"gateway": "192.168.1.1,192.168.1.4",
|
||||
"subnet": "192.168.2.0/24,192.168.1.250/24",
|
||||
},
|
||||
expectedError: "cannot configure multiple ranges (192.168.1.200/24, 192.168.1.0/24) on the same subnet (192.168.1.250/24)",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "255.255.200.0/24,255.255.120.0/24",
|
||||
"gateway": "255.0.255.0/24",
|
||||
"subnet": "255.255.255.0/24,255.255.0.255/24",
|
||||
},
|
||||
expectedError: "no matching subnet for range 255.255.200.0/24",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "192.168.1.0/24",
|
||||
"gateway": "192.168.1.1,192.168.1.4",
|
||||
"subnet": "192.168.2.0/24,192.168.1.250/24",
|
||||
},
|
||||
expectedError: "cannot configure multiple gateways (192.168.1.4, 192.168.1.1) for the same subnet (192.168.1.250/24)",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"ip-range": "192.168.1.0/24",
|
||||
"gateway": "192.168.4.1,192.168.5.4",
|
||||
"subnet": "192.168.2.0/24,192.168.1.250/24",
|
||||
},
|
||||
expectedError: "no matching subnet for gateway 192.168.4.1",
|
||||
},
|
||||
{
|
||||
args: []string{"toto"},
|
||||
flags: map[string]string{
|
||||
"gateway": "255.255.0.0/24",
|
||||
"subnet": "255.255.0.0/24",
|
||||
"aux-address": "255.255.0.30/24",
|
||||
},
|
||||
expectedError: "no matching subnet for aux-address",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
cmd := newCreateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
networkCreateFunc: tc.networkCreateFunc,
|
||||
}),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
require.NoError(t, cmd.Flags().Set(key, value))
|
||||
}
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
|
||||
}
|
||||
}
|
||||
func TestNetworkCreateWithFlags(t *testing.T) {
|
||||
expectedDriver := "foo"
|
||||
expectedOpts := []network.IPAMConfig{
|
||||
{
|
||||
"192.168.4.0/24",
|
||||
"192.168.4.0/24",
|
||||
"192.168.4.1/24",
|
||||
map[string]string{},
|
||||
},
|
||||
}
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
networkCreateFunc: func(ctx context.Context, name string, createBody types.NetworkCreate) (types.NetworkCreateResponse, error) {
|
||||
assert.Equal(t, expectedDriver, createBody.Driver, "not expected driver error")
|
||||
assert.Equal(t, expectedOpts, createBody.IPAM.Config, "not expected driver error")
|
||||
return types.NetworkCreateResponse{
|
||||
ID: name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
args := []string{"banana"}
|
||||
cmd := newCreateCommand(cli)
|
||||
|
||||
cmd.SetArgs(args)
|
||||
cmd.Flags().Set("driver", "foo")
|
||||
cmd.Flags().Set("ip-range", "192.168.4.0/24")
|
||||
cmd.Flags().Set("gateway", "192.168.4.1/24")
|
||||
cmd.Flags().Set("subnet", "192.168.4.0/24")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, "banana", strings.TrimSpace(cli.OutBuffer().String()))
|
||||
}
|
||||
@ -14,7 +14,7 @@ type disconnectOptions struct {
|
||||
force bool
|
||||
}
|
||||
|
||||
func newDisconnectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newDisconnectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
opts := disconnectOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -34,7 +34,7 @@ func newDisconnectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runDisconnect(dockerCli command.Cli, opts disconnectOptions) error {
|
||||
func runDisconnect(dockerCli *command.DockerCli, opts disconnectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
return client.NetworkDisconnect(context.Background(), opts.network, opts.container, opts.force)
|
||||
|
||||
@ -6,7 +6,6 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -16,7 +15,7 @@ type inspectOptions struct {
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -35,13 +34,13 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInspect(dockerCli command.Cli, opts inspectOptions) error {
|
||||
func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
|
||||
client := dockerCli.Client()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
getNetFunc := func(name string) (interface{}, []byte, error) {
|
||||
return client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{Verbose: opts.verbose})
|
||||
return client.NetworkInspectWithRaw(ctx, name, opts.verbose)
|
||||
}
|
||||
|
||||
return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc)
|
||||
|
||||
@ -25,7 +25,7 @@ type listOptions struct {
|
||||
filter opts.FilterOpt
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
options := listOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -47,7 +47,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(dockerCli command.Cli, options listOptions) error {
|
||||
func runList(dockerCli *command.DockerCli, options listOptions) error {
|
||||
client := dockerCli.Client()
|
||||
listOptions := types.NetworkListOptions{Filters: options.filter.Value()}
|
||||
networkResources, err := client.NetworkList(context.Background(), listOptions)
|
||||
|
||||
@ -7,11 +7,10 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm NETWORK [NETWORK...]",
|
||||
Aliases: []string{"remove"},
|
||||
@ -28,13 +27,13 @@ const ingressWarning = "WARNING! Before removing the routing-mesh network, " +
|
||||
"Otherwise, removal may not be effective and functionality of newly create " +
|
||||
"ingress networks will be impaired.\nAre you sure you want to continue?"
|
||||
|
||||
func runRemove(dockerCli command.Cli, networks []string) error {
|
||||
func runRemove(dockerCli *command.DockerCli, networks []string) error {
|
||||
client := dockerCli.Client()
|
||||
ctx := context.Background()
|
||||
status := 0
|
||||
|
||||
for _, name := range networks {
|
||||
if nw, _, err := client.NetworkInspectWithRaw(ctx, name, types.NetworkInspectOptions{}); err == nil &&
|
||||
if nw, _, err := client.NetworkInspectWithRaw(ctx, name, false); err == nil &&
|
||||
nw.Ingress &&
|
||||
!command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) {
|
||||
continue
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
@ -39,11 +40,12 @@ func TestNodeDemoteErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newDemoteCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
nodeUpdateFunc: tc.nodeUpdateFunc,
|
||||
}))
|
||||
}, buf))
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
@ -51,6 +53,7 @@ func TestNodeDemoteErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeDemoteNoChange(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newDemoteCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: func() (swarm.Node, []byte, error) {
|
||||
@ -62,12 +65,13 @@ func TestNodeDemoteNoChange(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"nodeID"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
}
|
||||
|
||||
func TestNodeDemoteMultipleNode(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newDemoteCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: func() (swarm.Node, []byte, error) {
|
||||
@ -79,7 +83,7 @@ func TestNodeDemoteMultipleNode(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}))
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"nodeID1", "nodeID2"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ func TestNodeInspectErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newInspectCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
}, buf))
|
||||
@ -111,7 +111,7 @@ func TestNodeInspectPretty(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newInspectCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"nodeID"})
|
||||
|
||||
@ -1,27 +1,15 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
"vbom.ml/util/sortorder"
|
||||
)
|
||||
|
||||
type byHostname []swarm.Node
|
||||
|
||||
func (n byHostname) Len() int { return len(n) }
|
||||
func (n byHostname) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
|
||||
func (n byHostname) Less(i, j int) bool {
|
||||
return sortorder.NaturalLess(n[i].Description.Hostname, n[j].Description.Hostname)
|
||||
}
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
@ -80,6 +68,5 @@ func runList(dockerCli command.Cli, options listOptions) error {
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewNodeFormat(format, options.quiet),
|
||||
}
|
||||
sort.Sort(byHostname(nodes))
|
||||
return formatter.NodeWrite(nodesCtx, nodes, info)
|
||||
}
|
||||
|
||||
@ -9,8 +9,6 @@ import (
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
@ -44,10 +42,12 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: tc.nodeListFunc,
|
||||
infoFunc: tc.infoFunc,
|
||||
})
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.EqualError(t, cmd.Execute(), tc.expectedError)
|
||||
@ -55,6 +55,7 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeList(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
@ -70,25 +71,25 @@ func TestNodeList(t *testing.T) {
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newListCommand(cli)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
out := cli.OutBuffer().String()
|
||||
assert.Contains(t, out, `nodeID1 * nodeHostname1 Ready Active Leader`)
|
||||
assert.Contains(t, out, `nodeID2 nodeHostname2 Ready Active Reachable`)
|
||||
assert.Contains(t, out, `nodeID3 nodeHostname3 Ready Active`)
|
||||
assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`)
|
||||
assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`)
|
||||
assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`)
|
||||
}
|
||||
|
||||
func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
*Node(),
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.Flags().Set("quiet", "true")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
@ -98,7 +99,8 @@ func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
|
||||
// Test case for #24090
|
||||
func TestNodeListContainsHostname(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{}, buf)
|
||||
cli := test.NewFakeCli(&fakeClient{}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newListCommand(cli)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Contains(t, buf.String(), "HOSTNAME")
|
||||
@ -106,7 +108,7 @@ func TestNodeListContainsHostname(t *testing.T) {
|
||||
|
||||
func TestNodeListDefaultFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
|
||||
@ -122,7 +124,7 @@ func TestNodeListDefaultFormat(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
cli.SetConfigfile(&configfile.ConfigFile{
|
||||
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
@ -134,7 +136,7 @@ func TestNodeListDefaultFormat(t *testing.T) {
|
||||
|
||||
func TestNodeListFormat(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cli := test.NewFakeCliWithOutput(&fakeClient{
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
|
||||
@ -149,7 +151,7 @@ func TestNodeListFormat(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}, buf)
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
cli.SetConfigfile(&configfile.ConfigFile{
|
||||
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
@ -158,22 +160,3 @@ func TestNodeListFormat(t *testing.T) {
|
||||
assert.Contains(t, buf.String(), `nodeHostname1: Leader`)
|
||||
assert.Contains(t, buf.String(), `nodeHostname2: Reachable`)
|
||||
}
|
||||
|
||||
func TestNodeListOrder(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
nodeListFunc: func() ([]swarm.Node, error) {
|
||||
return []swarm.Node{
|
||||
*Node(Hostname("node-2-foo"), Manager(Leader())),
|
||||
*Node(Hostname("node-10-foo"), Manager()),
|
||||
*Node(Hostname("node-1-foo")),
|
||||
}, nil
|
||||
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
actual := cli.OutBuffer().String()
|
||||
expected := golden.Get(t, []byte(actual), "node-list-sort.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ type nodeOptions struct {
|
||||
}
|
||||
|
||||
type annotations struct {
|
||||
name string
|
||||
labels opts.ListOpts
|
||||
}
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ func TestNodePromoteErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newPromoteCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
nodeUpdateFunc: tc.nodeUpdateFunc,
|
||||
}, buf))
|
||||
@ -55,7 +55,7 @@ func TestNodePromoteErrors(t *testing.T) {
|
||||
func TestNodePromoteNoChange(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newPromoteCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: func() (swarm.Node, []byte, error) {
|
||||
return *Node(Manager()), []byte{}, nil
|
||||
},
|
||||
@ -73,7 +73,7 @@ func TestNodePromoteNoChange(t *testing.T) {
|
||||
func TestNodePromoteMultipleNode(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newPromoteCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: func() (swarm.Node, []byte, error) {
|
||||
return *Node(), []byte{}, nil
|
||||
},
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/cli/command/task"
|
||||
"github.com/docker/cli/opts"
|
||||
@ -87,7 +88,11 @@ func runPs(dockerCli command.Cli, options psOptions) error {
|
||||
|
||||
format := options.format
|
||||
if len(format) == 0 {
|
||||
format = task.DefaultFormat(dockerCli.ConfigFile(), options.quiet)
|
||||
if dockerCli.ConfigFile() != nil && len(dockerCli.ConfigFile().TasksFormat) > 0 && !options.quiet {
|
||||
format = dockerCli.ConfigFile().TasksFormat
|
||||
} else {
|
||||
format = formatter.TableFormatKey
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 || len(tasks) != 0 {
|
||||
|
||||
@ -52,7 +52,7 @@ func TestNodePsErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newPsCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
taskInspectFunc: tc.taskInspectFunc,
|
||||
@ -103,11 +103,11 @@ func TestNodePs(t *testing.T) {
|
||||
},
|
||||
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
|
||||
return []swarm.Task{
|
||||
*Task(TaskID("taskID1"), TaskServiceID("failure"),
|
||||
*Task(TaskID("taskID1"), ServiceID("failure"),
|
||||
WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))),
|
||||
*Task(TaskID("taskID2"), TaskServiceID("failure"),
|
||||
*Task(TaskID("taskID2"), ServiceID("failure"),
|
||||
WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))),
|
||||
*Task(TaskID("taskID3"), TaskServiceID("failure"),
|
||||
*Task(TaskID("taskID3"), ServiceID("failure"),
|
||||
WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))),
|
||||
}, nil
|
||||
},
|
||||
@ -116,7 +116,7 @@ func TestNodePs(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newPsCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
infoFunc: tc.infoFunc,
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
taskInspectFunc: tc.taskInspectFunc,
|
||||
|
||||
@ -31,7 +31,7 @@ func TestNodeRemoveErrors(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newRemoveCommand(
|
||||
test.NewFakeCliWithOutput(&fakeClient{
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeRemoveFunc: tc.nodeRemoveFunc,
|
||||
}, buf))
|
||||
cmd.SetArgs(tc.args)
|
||||
@ -42,7 +42,7 @@ func TestNodeRemoveErrors(t *testing.T) {
|
||||
|
||||
func TestNodeRemoveMultiple(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newRemoveCommand(test.NewFakeCliWithOutput(&fakeClient{}, buf))
|
||||
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf))
|
||||
cmd.SetArgs([]string{"nodeID1", "nodeID2"})
|
||||
assert.NoError(t, cmd.Execute())
|
||||
}
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
node-1-foo:
|
||||
node-2-foo: Leader
|
||||
node-10-foo: Reachable
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
@ -94,7 +95,7 @@ func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error {
|
||||
}
|
||||
if flags.Changed(flagLabelAdd) {
|
||||
labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll()
|
||||
for k, v := range opts.ConvertKVStringsToMap(labels) {
|
||||
for k, v := range runconfigopts.ConvertKVStringsToMap(labels) {
|
||||
spec.Annotations.Labels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
@ -56,11 +57,12 @@ func TestNodeUpdateErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newUpdateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
nodeUpdateFunc: tc.nodeUpdateFunc,
|
||||
}))
|
||||
}, buf))
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
@ -156,11 +158,12 @@ func TestNodeUpdate(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newUpdateCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
nodeInspectFunc: tc.nodeInspectFunc,
|
||||
nodeUpdateFunc: tc.nodeUpdateFunc,
|
||||
}))
|
||||
}, buf))
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
cmd.Flags().Set(key, value)
|
||||
|
||||
51
components/cli/cli/command/prune/prune.go
Normal file
51
components/cli/cli/command/prune/prune.go
Normal file
@ -0,0 +1,51 @@
|
||||
package prune
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewContainerPruneCommand returns a cobra prune command for containers
|
||||
func NewContainerPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return container.NewPruneCommand(dockerCli)
|
||||
}
|
||||
|
||||
// NewVolumePruneCommand returns a cobra prune command for volumes
|
||||
func NewVolumePruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return volume.NewPruneCommand(dockerCli)
|
||||
}
|
||||
|
||||
// NewImagePruneCommand returns a cobra prune command for images
|
||||
func NewImagePruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return image.NewPruneCommand(dockerCli)
|
||||
}
|
||||
|
||||
// NewNetworkPruneCommand returns a cobra prune command for Networks
|
||||
func NewNetworkPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return network.NewPruneCommand(dockerCli)
|
||||
}
|
||||
|
||||
// RunContainerPrune executes a prune command for containers
|
||||
func RunContainerPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return container.RunPrune(dockerCli, filter)
|
||||
}
|
||||
|
||||
// RunVolumePrune executes a prune command for volumes
|
||||
func RunVolumePrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return volume.RunPrune(dockerCli, filter)
|
||||
}
|
||||
|
||||
// RunImagePrune executes a prune command for images
|
||||
func RunImagePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return image.RunPrune(dockerCli, all, filter)
|
||||
}
|
||||
|
||||
// RunNetworkPrune executes a prune command for networks
|
||||
func RunNetworkPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return network.RunPrune(dockerCli, filter)
|
||||
}
|
||||
@ -28,9 +28,7 @@ func ElectAuthServer(ctx context.Context, cli Cli) string {
|
||||
// the default registry URL might be Windows specific.
|
||||
serverAddress := registry.IndexServer
|
||||
if info, err := cli.Client().Info(ctx); err != nil {
|
||||
fmt.Fprintf(cli.Err(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress)
|
||||
} else if info.IndexServerAddress == "" {
|
||||
fmt.Fprintf(cli.Err(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", serverAddress)
|
||||
fmt.Fprintf(cli.Out(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress)
|
||||
} else {
|
||||
serverAddress = info.IndexServerAddress
|
||||
}
|
||||
@ -70,7 +68,7 @@ func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexI
|
||||
configKey = ElectAuthServer(ctx, cli)
|
||||
}
|
||||
|
||||
a, _ := cli.ConfigFile().GetAuthConfig(configKey)
|
||||
a, _ := cli.CredentialsStore(configKey).Get(configKey)
|
||||
return a
|
||||
}
|
||||
|
||||
@ -85,7 +83,7 @@ func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultR
|
||||
serverAddress = registry.ConvertToHostname(serverAddress)
|
||||
}
|
||||
|
||||
authconfig, err := cli.ConfigFile().GetAuthConfig(serverAddress)
|
||||
authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress)
|
||||
if err != nil {
|
||||
return authconfig, err
|
||||
}
|
||||
|
||||
@ -2,8 +2,6 @@ package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
@ -18,7 +16,7 @@ type loginOptions struct {
|
||||
serverAddress string
|
||||
user string
|
||||
password string
|
||||
passwordStdin bool
|
||||
email string
|
||||
}
|
||||
|
||||
// NewLoginCommand creates a new `docker login` command
|
||||
@ -42,7 +40,10 @@ func NewLoginCommand(dockerCli command.Cli) *cobra.Command {
|
||||
|
||||
flags.StringVarP(&opts.user, "username", "u", "", "Username")
|
||||
flags.StringVarP(&opts.password, "password", "p", "", "Password")
|
||||
flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin")
|
||||
|
||||
// Deprecated in 1.11: Should be removed in docker 17.06
|
||||
flags.StringVarP(&opts.email, "email", "e", "", "Email")
|
||||
flags.MarkDeprecated("email", "will be removed in 17.06.")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -51,27 +52,6 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
|
||||
ctx := context.Background()
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
if opts.password != "" {
|
||||
fmt.Fprintln(dockerCli.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
|
||||
if opts.passwordStdin {
|
||||
return errors.New("--password and --password-stdin are mutually exclusive")
|
||||
}
|
||||
}
|
||||
|
||||
if opts.passwordStdin {
|
||||
if opts.user == "" {
|
||||
return errors.New("Must provide --username with --password-stdin")
|
||||
}
|
||||
|
||||
contents, err := ioutil.ReadAll(dockerCli.In())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.password = strings.TrimSuffix(string(contents), "\n")
|
||||
opts.password = strings.TrimSuffix(opts.password, "\r")
|
||||
}
|
||||
|
||||
var (
|
||||
serverAddress string
|
||||
authServer = command.ElectAuthServer(ctx, dockerCli)
|
||||
@ -96,7 +76,7 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error {
|
||||
authConfig.Password = ""
|
||||
authConfig.IdentityToken = response.IdentityToken
|
||||
}
|
||||
if err := dockerCli.ConfigFile().GetCredentialsStore(serverAddress).Store(authConfig); err != nil {
|
||||
if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil {
|
||||
return errors.Errorf("Error saving credentials: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ func runLogout(dockerCli command.Cli, serverAddress string) error {
|
||||
|
||||
fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress)
|
||||
for _, r := range regsToLogout {
|
||||
if err := dockerCli.ConfigFile().GetCredentialsStore(r).Erase(r); err != nil {
|
||||
if err := dockerCli.CredentialsStore(r).Erase(r); err != nil {
|
||||
fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user