Compare commits
411 Commits
v17.12.1-c
...
v17.06.2-c
| Author | SHA1 | Date | |
|---|---|---|---|
| cec0b72a99 | |||
| ee7dfb4373 | |||
| eedf92a47a | |||
| a3cf1fb301 | |||
| 34d73cd0d8 | |||
| e31c8d8a5c | |||
| 6e39a2cb71 | |||
| 638a87493e | |||
| 5f8026ab88 | |||
| 6988f4f23f | |||
| 896a9ffc6b | |||
| 6c44271690 | |||
| 7294ad7f3c | |||
| 4661389bd3 | |||
| d25b1a3913 | |||
| 874a7374f3 | |||
| d7fe3fc7eb | |||
| 6b2e31a753 | |||
| 29d9279ad4 | |||
| 99a27323fe | |||
| 4770c7af51 | |||
| 0a33916f54 | |||
| 904cba3a81 | |||
| c1e16c7489 | |||
| d4e48884dd | |||
| df3f5ebc1e | |||
| 54ed530fdd | |||
| d44a878765 | |||
| 82e7ed09e3 | |||
| 797ee59557 | |||
| db53a4b0a9 | |||
| 280ee18747 | |||
| a703a55a77 | |||
| 5dbe00ec7c | |||
| 6082f71f15 | |||
| 8e0f32c269 | |||
| 2867d1282c | |||
| 7f68e988e4 | |||
| 292eac64d7 | |||
| 1930711f45 | |||
| c1503b058d | |||
| 08552809a0 | |||
| 87a7a648df | |||
| afe2c1b4bc | |||
| 9b2a406435 | |||
| 2a1db025ff | |||
| 7cd7d0794f | |||
| 07c249a12a | |||
| 344dbf919d | |||
| aa4e506101 | |||
| 8738f29e5b | |||
| 939ee76a3f | |||
| 271aa92890 | |||
| 9df8f75ba9 | |||
| 37ac42fbad | |||
| f2d5210861 | |||
| 1282b9439c | |||
| 7ddc25ce47 | |||
| 07ba784b1a | |||
| 2e360495e8 | |||
| 6345212aa2 | |||
| 983100a0e3 | |||
| 60340beefd | |||
| 8dcb831f11 | |||
| 7d230371d9 | |||
| 2b8fb89ff3 | |||
| dbe03d4016 | |||
| 08b100f7a1 | |||
| ac0d111d0e | |||
| fd33a51310 | |||
| ce7517d9a3 | |||
| 1473714177 | |||
| ea04a0dd40 | |||
| 21205dab24 | |||
| 96d84243ab | |||
| fc2a71dc69 | |||
| fb93aa0718 | |||
| d5cbd105be | |||
| 9666bdf780 | |||
| eafd4433aa | |||
| 903138e5fc | |||
| fcc60a6e76 | |||
| 96bbcbffdc | |||
| 69c6f58011 | |||
| eb669c3130 | |||
| 366839a5ac | |||
| b6df63d5f6 | |||
| 81f2f2c3d5 | |||
| 4b8be8eb56 | |||
| 375cbd92f2 | |||
| b0019a477a | |||
| 11cbcdbedd | |||
| 627ef8c382 | |||
| b379923129 | |||
| a0a268737c | |||
| 114d979f77 | |||
| 7ee46d779c | |||
| 26ace597a5 | |||
| 7fd4d9d5aa | |||
| caf6943211 | |||
| 02a80c5c3c | |||
| 1c82e267a7 | |||
| 1828a8bff1 | |||
| b7693dda2d | |||
| 15acd33ef6 | |||
| f2332481a7 | |||
| c2b2ea1702 | |||
| 068aa8ed63 | |||
| 9f144cca26 | |||
| bf1f505a77 | |||
| 0cd4fd721d | |||
| 860f79a6d4 | |||
| 7f0cfc1090 | |||
| 513bac34fe | |||
| 7b48a2700d | |||
| f30bfbf6c4 | |||
| 8e88731cf2 | |||
| 9ec7c03cf2 | |||
| e0fe1f5bf6 | |||
| 82dc9301a9 | |||
| 4e71fcacb8 | |||
| 19fe49f4b2 | |||
| 8bdf679283 | |||
| 8ea396ed7f | |||
| 358c36e930 | |||
| 51acf33ae3 | |||
| b31456f2cb | |||
| 77b4dce066 | |||
| 9cb6e86615 | |||
| 4581021c2e | |||
| 70709d0056 | |||
| 54ea1fe30f | |||
| 06dcd93778 | |||
| 1d2cd8d7ab | |||
| c257617b99 | |||
| 06f434c72b | |||
| 1d1b9ad4db | |||
| ab734f59cc | |||
| b538302e2c | |||
| 021cd82205 | |||
| 3c4292da1e | |||
| 10aa812ef5 | |||
| 9795a158bd | |||
| fb319d1554 | |||
| 3d947e7a2d | |||
| 31875af469 | |||
| 136b90fbd0 | |||
| 9241ec7a1e | |||
| aaf863d88a | |||
| 2d8beb7256 | |||
| 7a50b06fea | |||
| 2e99eed6f1 | |||
| cb9b544edf | |||
| 1e1c6b20ef | |||
| dfe4a1d9f2 | |||
| abfc72adff | |||
| b5f090a4fe | |||
| 2206628703 | |||
| 068a4285c6 | |||
| f1d6145f7b | |||
| 0aba54445b | |||
| 75ef04b420 | |||
| 5c477f0e07 | |||
| b2eb1339dc | |||
| 4ff004bf0d | |||
| b0efd8dda5 | |||
| 8cf887b304 | |||
| 5b4a4cc549 | |||
| 8e17167c78 | |||
| fd95ab90ad | |||
| 8498135904 | |||
| 15c737a076 | |||
| c1383568b1 | |||
| 3d418e35c9 | |||
| 1c96c7a5ec | |||
| 224f23149e | |||
| 8f5b746fdd | |||
| 9f5039a8d0 | |||
| 482d9bdee0 | |||
| f6b5934f71 | |||
| 7fe174d3f0 | |||
| 425242f524 | |||
| 78debca270 | |||
| 38f7bacec2 | |||
| bac91bd031 | |||
| 371fed7778 | |||
| 3218b57038 | |||
| 73b8fa7bc6 | |||
| 4295f109dc | |||
| 2036e34692 | |||
| 4ff90f5cb1 | |||
| af26684564 | |||
| f7a9f9fb67 | |||
| 647b4aecce | |||
| 056540a3f1 | |||
| 9abebe3dd0 | |||
| 149c1a0435 | |||
| 2cf204719f | |||
| b237329009 | |||
| 7c374afda6 | |||
| d4fe7965d2 | |||
| ee07daead1 | |||
| 84d2a56ff3 | |||
| e3d07855d5 | |||
| 1d69626e30 | |||
| 1c48508832 | |||
| 345e74062e | |||
| 6a52ac6c49 | |||
| b0d4910c7e | |||
| 73badb5bc7 | |||
| c61737080b | |||
| 728c4a5bfe | |||
| e529da3b02 | |||
| 7bd7bdeada | |||
| f9cb0563ec | |||
| dee078ae96 | |||
| 9f64b19d32 | |||
| 8be8aabad2 | |||
| db292393e5 | |||
| 067a30c4e7 | |||
| 87a974623f | |||
| 694bae5d49 | |||
| f8f518c6b7 | |||
| 3dc403d562 | |||
| a84cf88e26 | |||
| fbc1b3070d | |||
| 5595be60a5 | |||
| d0546cf484 | |||
| 02c1d87617 | |||
| f6937cf5b5 | |||
| 132f614ef3 | |||
| b7e417335c | |||
| bb8c1249be | |||
| 19db96873d | |||
| 5303070e05 | |||
| 672ea2ab2a | |||
| eb990142ac | |||
| 55d38f9cd6 | |||
| 29e8b830bb | |||
| 2df40e7778 | |||
| ef8fe31895 | |||
| 77c3cf33ae | |||
| 35e513d55c | |||
| cb764708de | |||
| 5578ebd2a2 | |||
| bb23c93d28 | |||
| 073d80bc46 | |||
| 91fd299b2c | |||
| a0040598f2 | |||
| 7219fddea4 | |||
| 29fcd5dfae | |||
| 9600fb4e78 | |||
| 0f80f28c1a | |||
| 29e7f7b8c9 | |||
| 4c2cf22362 | |||
| afcb08cfb0 | |||
| 46a9d6b041 | |||
| b8c7116a23 | |||
| b089783b38 | |||
| 605ffe91cf | |||
| eb837fd7bc | |||
| 475c218fd7 | |||
| f2eb8b825b | |||
| 1891675559 | |||
| 7953dbc649 | |||
| 5c94f3eeda | |||
| d96b9b7edf | |||
| be1ea9771c | |||
| 5d8e13a06c | |||
| d611e35db6 | |||
| 85aaa4f261 | |||
| 56ecfabb9f | |||
| 97a704b2a7 | |||
| a16a6004a8 | |||
| 17c2a50117 | |||
| 5e671f7b53 | |||
| 986443bad2 | |||
| eaac30972d | |||
| 6f00f5603d | |||
| ad098a7a03 | |||
| 9c1f95529e | |||
| 8c2c3e21a3 | |||
| da2d17a18f | |||
| 8ef3d608d9 | |||
| 514d89f021 | |||
| 0189f9d259 | |||
| ca7df6fe42 | |||
| 47c1b00158 | |||
| f7e9f2cefa | |||
| 960ddece48 | |||
| 5a5e9e32bd | |||
| eff2539693 | |||
| e92c400a01 | |||
| 5d4843baa3 | |||
| cfb8aafe5e | |||
| 4e1370233e | |||
| 091732e350 | |||
| 1af533909d | |||
| a014274b80 | |||
| 7b65e51031 | |||
| ab5798a6f7 | |||
| 899ed1b641 | |||
| a9a1a9c7de | |||
| 5ed0b2e10c | |||
| 7950a39242 | |||
| 5bd868da90 | |||
| e142124317 | |||
| 0fa9fe7713 | |||
| 69fc734572 | |||
| 5e405a94e8 | |||
| 897b692e1c | |||
| d8fc4f1ad2 | |||
| af4ae65a0d | |||
| c8fa12c15c | |||
| 36f4ffb042 | |||
| 0cbae9b2ee | |||
| dcd1f685c8 | |||
| 05648d36b5 | |||
| e1ebcf33f6 | |||
| 4ea6f802a5 | |||
| be7a008421 | |||
| fcd821892e | |||
| 76b0c4884b | |||
| 1f928815e5 | |||
| ad8c94e585 | |||
| 058e676c7f | |||
| 11637f7d81 | |||
| 5965f0216f | |||
| 3910d5b571 | |||
| 03d8258b7d | |||
| 1c2ce3d977 | |||
| eb4ef82087 | |||
| d09575fb8f | |||
| 13934b618c | |||
| e9b6305e1d | |||
| 94f4d72c55 | |||
| 765e46f7cc | |||
| f15c4fd160 | |||
| 66d168dd55 | |||
| 59d6ed0a4d | |||
| 402dd4a9ea | |||
| 14ff8286f7 | |||
| eb1565bba1 | |||
| de513d091d | |||
| 1d4b51cccb | |||
| b5411da4de | |||
| 7239874782 | |||
| 18355074d7 | |||
| 1f94b6c7ec | |||
| 737e92d753 | |||
| 97b6afddfe | |||
| 00327e2c1c | |||
| 0dbee88694 | |||
| ce44edfbf8 | |||
| 0efdcc403e | |||
| 8cf591e57d | |||
| 5cfdad44b3 | |||
| 5bb86daf51 | |||
| 9b8ffe709d | |||
| 8928dccaa6 | |||
| 972de9a657 | |||
| 9dad38d70a | |||
| 699fcc1433 | |||
| 40d95168e1 | |||
| d236b522c5 | |||
| 111c39aff1 | |||
| c1c839c28b | |||
| f650eb38a6 | |||
| aa3a4c2846 | |||
| 6a52ceebf4 | |||
| c6dd1afaa6 | |||
| f148100b07 | |||
| 8ce2c283f7 | |||
| b2dacf38db | |||
| 6c636490a3 | |||
| c1f7e41e4e | |||
| a148bf60af | |||
| 4910da9ddb | |||
| 9e9c4b52b9 | |||
| fff42c853a | |||
| 2bdf7010a1 | |||
| 46f679cc1d | |||
| 3a237eae5f | |||
| 8330e07f4a | |||
| 9a732dbff3 | |||
| 08cb13b09b | |||
| 09185931e1 | |||
| 8d44c9a703 | |||
| 0334cfffff | |||
| a9548340cb | |||
| 91d25d6fe0 | |||
| 6c534bb5a0 | |||
| 1c46b8f4d3 | |||
| f156aa746e | |||
| 5c5cc1fccd | |||
| cd6b682d4f | |||
| 19e7c1efe9 | |||
| 32807d0a87 | |||
| e7776047c2 | |||
| 30e966bc97 | |||
| bb1fe809d2 | |||
| e724dcd16b | |||
| 96c9cd5c39 | |||
| 4360062d33 | |||
| 5acb14a901 | |||
| 43484d9284 | |||
| 7f8486a39a | |||
| 5f615dd295 | |||
| f3810787c8 | |||
| 2bcfe6ffc2 | |||
| 0b4548c769 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +0,0 @@
|
||||
.helpers/
|
||||
3835
CHANGELOG.md
3835
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
120
CONTRIBUTING.md
120
CONTRIBUTING.md
@ -2,125 +2,5 @@
|
||||
|
||||
Want to contribute on Docker CE? Awesome!
|
||||
|
||||
This page contains information about reporting issues as well as some tips and
|
||||
guidelines useful to experienced open source contributors. Finally, make sure
|
||||
you read our [community guidelines](#docker-community-guidelines) before you
|
||||
start participating.
|
||||
|
||||
## Topics
|
||||
|
||||
* [Reporting Security Issues](#reporting-security-issues)
|
||||
* [Reporting Issues](#reporting-other-issues)
|
||||
* [Submitting Pull Requests](#submitting-pull-requests)
|
||||
* [Community Guidelines](#docker-community-guidelines)
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
The Docker maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
Please **DO NOT** file a public issue, instead send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||
We also like to send gifts—if you're into Docker schwag, make sure to let
|
||||
us know. We currently do not offer a paid security bounty program, but are not
|
||||
ruling it out in the future.
|
||||
|
||||
## Reporting other 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
|
||||
|
||||
When reporting issues, always include:
|
||||
|
||||
* The output of `docker version`.
|
||||
* The output of `docker info`.
|
||||
|
||||
If presented with a template when creating an issue, please follow its directions.
|
||||
|
||||
Also include the steps required to reproduce the problem if possible and
|
||||
applicable. This information will help us review and fix your issue faster.
|
||||
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
|
||||
Don't forget to remove sensitive data from your logfiles before posting (you can
|
||||
replace those parts with "REDACTED").
|
||||
|
||||
## Submitting pull requests
|
||||
|
||||
Please see the corresponding `CONTRIBUTING.md` file of each component for more information:
|
||||
|
||||
* 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
|
||||
|
||||
## Docker community guidelines
|
||||
|
||||
We want to keep the Docker community awesome, growing and collaborative. We need
|
||||
your help to keep it that way. To help with this we've come up with some general
|
||||
guidelines for the community as a whole:
|
||||
|
||||
* Be nice: Be courteous, respectful and polite to fellow community members.
|
||||
Regional, racial, gender, or other abuse will not be tolerated. We like
|
||||
nice people way better than mean ones!
|
||||
|
||||
* Encourage diversity and participation: Make everyone in our community feel
|
||||
welcome, regardless of their background and the extent of their
|
||||
contributions, and do everything possible to encourage participation in
|
||||
our community.
|
||||
|
||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
||||
you own, do not share private or sensitive information, and don't break
|
||||
the law.
|
||||
|
||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||
avoid off-topic discussions. Remember when you update an issue or respond
|
||||
to an email you are potentially sending to a large number of people. Please
|
||||
consider this before you update. Also remember that nobody likes spam.
|
||||
|
||||
* Don't send email to the maintainers: There's no need to send email to the
|
||||
maintainers to ask them to investigate an issue or to take a look at a
|
||||
pull request. Instead of sending an email, GitHub mentions should be
|
||||
used to ping maintainers to review a pull request, a proposal or an
|
||||
issue.
|
||||
|
||||
### Guideline violations — 3 strikes method
|
||||
|
||||
The point of this section is not to find opportunities to punish people, but we
|
||||
do need a fair way to deal with people who are making our community suck.
|
||||
|
||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
||||
behavior is inappropriate according to our guidelines.
|
||||
|
||||
2. Second occurrence: We will send you a private message with a warning that
|
||||
any additional violations will result in removal from the community.
|
||||
|
||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
||||
your account.
|
||||
|
||||
**Notes:**
|
||||
|
||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
||||
have spam all over the place.
|
||||
|
||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
||||
grudge.
|
||||
|
||||
* People who commit minor infractions will get some education, rather than
|
||||
hammering them in the 3 strikes process.
|
||||
|
||||
* The rules apply equally to everyone in the community, no matter how much
|
||||
you've contributed.
|
||||
|
||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
||||
|
||||
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
|
||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
||||
fair solution if there has been a misunderstanding.
|
||||
30
Makefile
30
Makefile
@ -1,56 +1,30 @@
|
||||
CLI_DIR:=$(CURDIR)/components/cli
|
||||
ENGINE_DIR:=$(CURDIR)/components/engine
|
||||
PACKAGING_DIR:=$(CURDIR)/components/packaging
|
||||
MOBY_COMPONENTS_SHA=ab7c118272b02d8672dc0255561d0c4015979780
|
||||
MOBY_COMPONENTS_URL=https://raw.githubusercontent.com/shykes/moby-extras/$(MOBY_COMPONENTS_SHA)/cmd/moby-components
|
||||
MOBY_COMPONENTS=.helpers/moby-components-$(MOBY_COMPONENTS_SHA)
|
||||
VERSION=$(shell cat VERSION)
|
||||
|
||||
.PHONY: help
|
||||
help: ## show make targets
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: test-integration-cli
|
||||
test-integration-cli: $(CLI_DIR)/build/docker ## test integration of cli and engine
|
||||
$(MAKE) -C $(ENGINE_DIR) DOCKER_CLI_PATH=$< test-integration-cli
|
||||
|
||||
$(CLI_DIR)/build/docker:
|
||||
$(MAKE) -C $(CLI_DIR) -f docker.Makefile build
|
||||
|
||||
.PHONY: deb
|
||||
deb: ## build deb packages
|
||||
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) deb
|
||||
|
||||
.PHONY: rpm
|
||||
rpm: ## build rpm packages
|
||||
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) rpm
|
||||
|
||||
.PHONY: static
|
||||
static: ## build static packages
|
||||
$(MAKE) VERSION=$(VERSION) CLI_DIR=$(CLI_DIR) ENGINE_DIR=$(ENGINE_DIR) -C $(PACKAGING_DIR) static
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## clean the build artifacts
|
||||
-$(MAKE) -C $(CLI_DIR) clean
|
||||
-$(MAKE) -C $(ENGINE_DIR) clean
|
||||
-$(MAKE) -C $(PACKAGING_DIR) clean
|
||||
|
||||
$(MOBY_COMPONENTS):
|
||||
mkdir -p .helpers
|
||||
curl -fsSL $(MOBY_COMPONENTS_URL) > $(MOBY_COMPONENTS)
|
||||
chmod +x $(MOBY_COMPONENTS)
|
||||
|
||||
.PHONY: update-components
|
||||
update-components: update-components-cli update-components-engine update-components-packaging ## udpate components using moby extra tool
|
||||
|
||||
.PHONY: update-components-cli
|
||||
update-components-cli: $(MOBY_COMPONENTS)
|
||||
$(MOBY_COMPONENTS) update cli
|
||||
|
||||
.PHONY: update-components-engine
|
||||
update-components-engine: $(MOBY_COMPONENTS)
|
||||
$(MOBY_COMPONENTS) update engine
|
||||
|
||||
.PHONY: update-components-packaging
|
||||
update-components-packaging: $(MOBY_COMPONENTS)
|
||||
$(MOBY_COMPONENTS) update packaging
|
||||
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
|
||||
|
||||
9
components/cli/.github/CODEOWNERS
vendored
9
components/cli/.github/CODEOWNERS
vendored
@ -1,9 +0,0 @@
|
||||
# GitHub code owners
|
||||
# See https://github.com/blog/2392-introducing-code-owners
|
||||
|
||||
cli/command/stack/** @dnephin @vdemeester
|
||||
cli/compose/** @dnephin @vdemeester
|
||||
contrib/completion/bash/** @albers
|
||||
contrib/completion/zsh/** @sdurrheimer
|
||||
docs/** @mistyhacks @vdemeester @thaJeztah
|
||||
scripts/** @dnephin
|
||||
2
components/cli/.gitignore
vendored
2
components/cli/.gitignore
vendored
@ -6,5 +6,3 @@ cli/winresources/rsrc_amd64.syso
|
||||
/man/man5/
|
||||
/man/man8/
|
||||
/docs/yaml/gen/
|
||||
coverage.txt
|
||||
profile.out
|
||||
|
||||
@ -1,365 +0,0 @@
|
||||
# Contributing to Docker
|
||||
|
||||
Want to hack on Docker? Awesome! We have a contributor's guide that explains
|
||||
[setting up a Docker development environment and the contribution
|
||||
process](https://docs.docker.com/opensource/project/who-written-for/).
|
||||
|
||||
This page contains information about reporting issues as well as some tips and
|
||||
guidelines useful to experienced open source contributors. Finally, make sure
|
||||
you read our [community guidelines](#docker-community-guidelines) before you
|
||||
start participating.
|
||||
|
||||
## Topics
|
||||
|
||||
* [Reporting Security Issues](#reporting-security-issues)
|
||||
* [Design and Cleanup Proposals](#design-and-cleanup-proposals)
|
||||
* [Reporting Issues](#reporting-other-issues)
|
||||
* [Quick Contribution Tips and Guidelines](#quick-contribution-tips-and-guidelines)
|
||||
* [Community Guidelines](#docker-community-guidelines)
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
The Docker maintainers take security seriously. If you discover a security
|
||||
issue, please bring it to their attention right away!
|
||||
|
||||
Please **DO NOT** file a public issue, instead send your report privately to
|
||||
[security@docker.com](mailto:security@docker.com).
|
||||
|
||||
Security reports are greatly appreciated and we will publicly thank you for it.
|
||||
We also like to send gifts—if you're into Docker schwag, make sure to let
|
||||
us know. We currently do not offer a paid security bounty program, but are not
|
||||
ruling it out in the future.
|
||||
|
||||
|
||||
## Reporting other issues
|
||||
|
||||
A great way to contribute to the project is to send a detailed report when you
|
||||
encounter an issue. We always appreciate a well-written, thorough bug report,
|
||||
and will thank you for it!
|
||||
|
||||
Check that [our issue database](https://github.com/docker/cli/issues)
|
||||
doesn't already include that problem or suggestion before submitting an issue.
|
||||
If you find a match, you can use the "subscribe" button to get notified on
|
||||
updates. Do *not* leave random "+1" or "I have this too" comments, as they
|
||||
only clutter the discussion, and don't help resolving it. However, if you
|
||||
have ways to reproduce the issue or have additional information that may help
|
||||
resolving the issue, please leave a comment.
|
||||
|
||||
When reporting issues, always include:
|
||||
|
||||
* The output of `docker version`.
|
||||
* The output of `docker info`.
|
||||
|
||||
Also include the steps required to reproduce the problem if possible and
|
||||
applicable. This information will help us review and fix your issue faster.
|
||||
When sending lengthy log-files, consider posting them as a gist (https://gist.github.com).
|
||||
Don't forget to remove sensitive data from your logfiles before posting (you can
|
||||
replace those parts with "REDACTED").
|
||||
|
||||
## Quick contribution tips and guidelines
|
||||
|
||||
This section gives the experienced contributor some tips and guidelines.
|
||||
|
||||
### Pull requests are always welcome
|
||||
|
||||
Not sure if that typo is worth a pull request? Found a bug and know how to fix
|
||||
it? Do it! We will appreciate it. Any significant improvement should be
|
||||
documented as [a GitHub issue](https://github.com/docker/cli/issues) before
|
||||
anybody starts working on it.
|
||||
|
||||
We are always thrilled to receive pull requests. We do our best to process them
|
||||
quickly. If your pull request is not accepted on the first try,
|
||||
don't get discouraged! Our contributor's guide explains [the review process we
|
||||
use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
|
||||
|
||||
### Talking to other Docker users and contributors
|
||||
|
||||
<table class="tg">
|
||||
<col width="45%">
|
||||
<col width="65%">
|
||||
<tr>
|
||||
<td>Forums</td>
|
||||
<td>
|
||||
A public forum for users to discuss questions and explore current design patterns and
|
||||
best practices about Docker and related projects in the Docker Ecosystem. To participate,
|
||||
just log in with your Docker Hub account on <a href="https://forums.docker.com" target="_blank">https://forums.docker.com</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Community Slack</td>
|
||||
<td>
|
||||
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://community.docker.com/registrations/groups/4316" target="_blank">with this link</a>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Twitter</td>
|
||||
<td>
|
||||
You can follow <a href="https://twitter.com/docker/" target="_blank">Docker's Twitter feed</a>
|
||||
to get updates on our products. You can also tweet us questions or just
|
||||
share blogs or stories.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack Overflow</td>
|
||||
<td>
|
||||
Stack Overflow has over 17000 Docker questions listed. We regularly
|
||||
monitor <a href="https://stackoverflow.com/search?tab=newest&q=docker" target="_blank">Docker questions</a>
|
||||
and so do many other knowledgeable Docker users.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
### Conventions
|
||||
|
||||
Fork the repository and make changes on your fork in a feature branch:
|
||||
|
||||
- If it's a bug fix branch, name it XXXX-something where XXXX is the number of
|
||||
the issue.
|
||||
- If it's a feature branch, create an enhancement issue to announce
|
||||
your intentions, and name it XXXX-something where XXXX is the number of the
|
||||
issue.
|
||||
|
||||
Submit unit tests for your changes. Go has a great test framework built in; use
|
||||
it! Take a look at existing tests for inspiration. [Run the full test
|
||||
suite](README.md) on your branch before
|
||||
submitting a pull request.
|
||||
|
||||
Update the documentation when creating or modifying features. Test your
|
||||
documentation changes for clarity, concision, and correctness, as well as a
|
||||
clean documentation build. See our contributors guide for [our style
|
||||
guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
|
||||
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
committing your changes. Most editors have plug-ins that do this automatically.
|
||||
|
||||
Pull request descriptions should be as clear as possible and include a reference
|
||||
to all the issues that they address.
|
||||
|
||||
Commit messages must start with a capitalized and short summary (max. 50 chars)
|
||||
written in the imperative, followed by an optional, more detailed explanatory
|
||||
text which is separated from the summary by an empty line.
|
||||
|
||||
Code review comments may be added to your pull request. Discuss, then make the
|
||||
suggested modifications and push additional commits to your feature branch. Post
|
||||
a comment after pushing. New commits show up in the pull request automatically,
|
||||
but the reviewers are notified only when you comment.
|
||||
|
||||
Pull requests must be cleanly rebased on top of master without multiple branches
|
||||
mixed into the PR.
|
||||
|
||||
**Git tip**: If your PR no longer merges cleanly, use `rebase master` in your
|
||||
feature branch to update your pull request rather than `merge master`.
|
||||
|
||||
Before you make a pull request, squash your commits into logical units of work
|
||||
using `git rebase -i` and `git push -f`. A logical unit of work is a consistent
|
||||
set of patches that should be reviewed together: for example, upgrading the
|
||||
version of a vendored dependency and taking advantage of its now available new
|
||||
feature constitute two separate units of work. Implementing a new function and
|
||||
calling it in another file constitute a single logical unit of work. The very
|
||||
high majority of submissions should have a single commit, so if in doubt: squash
|
||||
down to one.
|
||||
|
||||
After every commit, make sure the test suite passes. Include documentation
|
||||
changes in the same pull request so that a revert would remove all traces of
|
||||
the feature or fix.
|
||||
|
||||
Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull request
|
||||
description that close an issue. Including references automatically closes the issue
|
||||
on a merge.
|
||||
|
||||
Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
|
||||
from the Git history.
|
||||
|
||||
Please see the [Coding Style](#coding-style) for further guidelines.
|
||||
|
||||
### Merge approval
|
||||
|
||||
Docker maintainers use LGTM (Looks Good To Me) in comments on the code review to
|
||||
indicate acceptance.
|
||||
|
||||
A change requires LGTMs from an absolute majority of the maintainers of each
|
||||
component affected. For example, if a change affects `docs/` and `registry/`, it
|
||||
needs an absolute majority from the maintainers of `docs/` AND, separately, an
|
||||
absolute majority of the maintainers of `registry/`.
|
||||
|
||||
For more details, see the [MAINTAINERS](MAINTAINERS) page.
|
||||
|
||||
### Sign your work
|
||||
|
||||
The sign-off is a simple line at the end of the explanation for the patch. Your
|
||||
signature certifies that you wrote the patch or otherwise have the right to pass
|
||||
it on as an open-source patch. The rules are pretty simple: if you can certify
|
||||
the below (from [developercertificate.org](http://developercertificate.org/)):
|
||||
|
||||
```
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
660 York Street, Suite 102,
|
||||
San Francisco, CA 94110 USA
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
```
|
||||
|
||||
Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
### How can I become a maintainer?
|
||||
|
||||
The procedures for adding new maintainers are explained in the
|
||||
global [MAINTAINERS](https://github.com/docker/opensource/blob/master/MAINTAINERS)
|
||||
file in the [https://github.com/docker/opensource/](https://github.com/docker/opensource/)
|
||||
repository.
|
||||
|
||||
Don't forget: being a maintainer is a time investment. Make sure you
|
||||
will have time to make yourself available. You don't have to be a
|
||||
maintainer to make a difference on the project!
|
||||
|
||||
## Docker community guidelines
|
||||
|
||||
We want to keep the Docker community awesome, growing and collaborative. We need
|
||||
your help to keep it that way. To help with this we've come up with some general
|
||||
guidelines for the community as a whole:
|
||||
|
||||
* Be nice: Be courteous, respectful and polite to fellow community members:
|
||||
no regional, racial, gender, or other abuse will be tolerated. We like
|
||||
nice people way better than mean ones!
|
||||
|
||||
* Encourage diversity and participation: Make everyone in our community feel
|
||||
welcome, regardless of their background and the extent of their
|
||||
contributions, and do everything possible to encourage participation in
|
||||
our community.
|
||||
|
||||
* Keep it legal: Basically, don't get us in trouble. Share only content that
|
||||
you own, do not share private or sensitive information, and don't break
|
||||
the law.
|
||||
|
||||
* Stay on topic: Make sure that you are posting to the correct channel and
|
||||
avoid off-topic discussions. Remember when you update an issue or respond
|
||||
to an email you are potentially sending to a large number of people. Please
|
||||
consider this before you update. Also remember that nobody likes spam.
|
||||
|
||||
* Don't send email to the maintainers: There's no need to send email to the
|
||||
maintainers to ask them to investigate an issue or to take a look at a
|
||||
pull request. Instead of sending an email, GitHub mentions should be
|
||||
used to ping maintainers to review a pull request, a proposal or an
|
||||
issue.
|
||||
|
||||
### Guideline violations — 3 strikes method
|
||||
|
||||
The point of this section is not to find opportunities to punish people, but we
|
||||
do need a fair way to deal with people who are making our community suck.
|
||||
|
||||
1. First occurrence: We'll give you a friendly, but public reminder that the
|
||||
behavior is inappropriate according to our guidelines.
|
||||
|
||||
2. Second occurrence: We will send you a private message with a warning that
|
||||
any additional violations will result in removal from the community.
|
||||
|
||||
3. Third occurrence: Depending on the violation, we may need to delete or ban
|
||||
your account.
|
||||
|
||||
**Notes:**
|
||||
|
||||
* Obvious spammers are banned on first occurrence. If we don't do this, we'll
|
||||
have spam all over the place.
|
||||
|
||||
* Violations are forgiven after 6 months of good behavior, and we won't hold a
|
||||
grudge.
|
||||
|
||||
* People who commit minor infractions will get some education, rather than
|
||||
hammering them in the 3 strikes process.
|
||||
|
||||
* The rules apply equally to everyone in the community, no matter how much
|
||||
you've contributed.
|
||||
|
||||
* Extreme violations of a threatening, abusive, destructive or illegal nature
|
||||
will be addressed immediately and are not subject to 3 strikes or forgiveness.
|
||||
|
||||
* Contact abuse@docker.com to report abuse or appeal violations. In the case of
|
||||
appeals, we know that mistakes happen, and we'll work with you to come up with a
|
||||
fair solution if there has been a misunderstanding.
|
||||
|
||||
## Coding Style
|
||||
|
||||
Unless explicitly stated, we follow all coding guidelines from the Go
|
||||
community. While some of these standards may seem arbitrary, they somehow seem
|
||||
to result in a solid, consistent codebase.
|
||||
|
||||
It is possible that the code base does not currently comply with these
|
||||
guidelines. We are not looking for a massive PR that fixes this, since that
|
||||
goes against the spirit of the guidelines. All new contributions should make a
|
||||
best effort to clean up and make the code base better than they left it.
|
||||
Obviously, apply your best judgement. Remember, the goal here is to make the
|
||||
code base easier for humans to navigate and understand. Always keep that in
|
||||
mind when nudging others to comply.
|
||||
|
||||
The rules:
|
||||
|
||||
1. All code should be formatted with `gofmt -s`.
|
||||
2. All code should pass the default levels of
|
||||
[`golint`](https://github.com/golang/lint).
|
||||
3. All code should follow the guidelines covered in [Effective
|
||||
Go](http://golang.org/doc/effective_go.html) and [Go Code Review
|
||||
Comments](https://github.com/golang/go/wiki/CodeReviewComments).
|
||||
4. Comment the code. Tell us the why, the history and the context.
|
||||
5. Document _all_ declarations and methods, even private ones. Declare
|
||||
expectations, caveats and anything else that may be important. If a type
|
||||
gets exported, having the comments already there will ensure it's ready.
|
||||
6. Variable name length should be proportional to its context and no longer.
|
||||
`noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`.
|
||||
In practice, short methods will have short variable names and globals will
|
||||
have longer names.
|
||||
7. No underscores in package names. If you need a compound name, step back,
|
||||
and re-examine why you need a compound name. If you still think you need a
|
||||
compound name, lose the underscore.
|
||||
8. No utils or helpers packages. If a function is not general enough to
|
||||
warrant its own package, it has not been written generally enough to be a
|
||||
part of a util package. Just leave it unexported and well-documented.
|
||||
9. All tests should run with `go test` and outside tooling should not be
|
||||
required. No, we don't need another unit testing framework. Assertion
|
||||
packages are acceptable if they provide _real_ incremental value.
|
||||
10. Even though we call these "rules" above, they are actually just
|
||||
guidelines. Since you've read all the rules, you now know that.
|
||||
|
||||
If you are having trouble getting into the mood of idiomatic Go, we recommend
|
||||
reading through [Effective Go](https://golang.org/doc/effective_go.html). The
|
||||
[Go Blog](https://blog.golang.org) is also a great resource. Drinking the
|
||||
kool-aid is a lot easier than going thirsty.
|
||||
12
components/cli/Jenkinsfile
vendored
12
components/cli/Jenkinsfile
vendored
@ -1,12 +0,0 @@
|
||||
wrappedNode(label: 'linux && x86_64', cleanWorkspace: true) {
|
||||
timeout(time: 60, unit: 'MINUTES') {
|
||||
stage "Git Checkout"
|
||||
checkout scm
|
||||
|
||||
stage "Run end-to-end test suite"
|
||||
sh "docker version"
|
||||
sh "E2E_UNIQUE_ID=clie2e${BUILD_NUMBER} \
|
||||
IMAGE_TAG=clie2e${BUILD_NUMBER} \
|
||||
make -f docker.Makefile test-e2e"
|
||||
}
|
||||
}
|
||||
@ -27,7 +27,6 @@
|
||||
|
||||
people = [
|
||||
"aaronlehmann",
|
||||
"albers",
|
||||
"aluzzardi",
|
||||
"anusha",
|
||||
"cpuguy83",
|
||||
@ -85,11 +84,6 @@
|
||||
Email = "aaron.lehmann@docker.com"
|
||||
GitHub = "aaronlehmann"
|
||||
|
||||
[people.albers]
|
||||
Name = "Harald Albers"
|
||||
Email = "github@albersweb.de"
|
||||
GitHub = "albers"
|
||||
|
||||
[people.aluzzardi]
|
||||
Name = "Andrea Luzzardi"
|
||||
Email = "al@docker.com"
|
||||
@ -103,7 +97,7 @@
|
||||
[people.cpuguy83]
|
||||
Name = "Brian Goff"
|
||||
Email = "cpuguy83@gmail.com"
|
||||
GitHub = "cpuguy83"
|
||||
Github = "cpuguy83"
|
||||
|
||||
[people.crosbymichael]
|
||||
Name = "Michael Crosby"
|
||||
@ -133,7 +127,7 @@
|
||||
[people.misty]
|
||||
Name = "Misty Stanley-Jones"
|
||||
Email = "misty@docker.com"
|
||||
GitHub = "mistyhacks"
|
||||
GitHub = "mstanleyjones"
|
||||
|
||||
[people.mlaventure]
|
||||
Name = "Kenfe-Mickaël Laventure"
|
||||
|
||||
@ -1,85 +1,59 @@
|
||||
#
|
||||
# github.com/docker/cli
|
||||
#
|
||||
|
||||
all: binary
|
||||
|
||||
|
||||
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
|
||||
|
||||
# remove build artifacts
|
||||
.PHONY: clean
|
||||
clean: ## remove build artifacts
|
||||
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: ## run unit test
|
||||
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
clean:
|
||||
@rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
|
||||
|
||||
# run go test
|
||||
# the "-tags daemon" part is temporary
|
||||
.PHONY: test
|
||||
test: test-unit ## run tests
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## run test coverage
|
||||
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
test:
|
||||
@go test -tags daemon -v $(shell go list ./... | grep -v '/vendor/')
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## run all the lint tools
|
||||
gometalinter --config gometalinter.json ./...
|
||||
lint:
|
||||
@gometalinter --config gometalinter.json ./...
|
||||
|
||||
.PHONY: binary
|
||||
binary: ## build executable for Linux
|
||||
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
|
||||
./scripts/build/binary
|
||||
binary:
|
||||
@./scripts/build/binary
|
||||
|
||||
# build the CLI for multiple architectures
|
||||
.PHONY: cross
|
||||
cross: ## build executable for macOS and Windows
|
||||
./scripts/build/cross
|
||||
|
||||
.PHONY: binary-windows
|
||||
binary-windows: ## build executable for Windows
|
||||
./scripts/build/windows
|
||||
|
||||
.PHONY: binary-osx
|
||||
binary-osx: ## build executable for macOS
|
||||
./scripts/build/osx
|
||||
cross:
|
||||
@./scripts/build/cross
|
||||
|
||||
.PHONY: dynbinary
|
||||
dynbinary: ## build dynamically linked binary
|
||||
./scripts/build/dynbinary
|
||||
dynbinary:
|
||||
@./scripts/build/dynbinary
|
||||
|
||||
.PHONY: watch
|
||||
watch: ## monitor file changes and run go test
|
||||
./scripts/test/watch
|
||||
watch:
|
||||
@./scripts/test/watch
|
||||
|
||||
vendor: vendor.conf ## check that vendor matches vendor.conf
|
||||
vndr 2> /dev/null
|
||||
scripts/validate/check-git-diff vendor
|
||||
# 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: ## generate man pages from go source and markdown
|
||||
scripts/docs/generate-man.sh
|
||||
manpages:
|
||||
@scripts/docs/generate-man.sh
|
||||
|
||||
## generate documentation YAML files consumed by docs repo
|
||||
.PHONY: yamldocs
|
||||
yamldocs: ## generate documentation YAML files consumed by docs repo
|
||||
scripts/docs/generate-yaml.sh
|
||||
|
||||
.PHONY: shellcheck
|
||||
shellcheck: ## run shellcheck validation
|
||||
scripts/validate/shellcheck
|
||||
|
||||
.PHONY: help
|
||||
help: ## print this help
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
yamldocs:
|
||||
@scripts/docs/generate-yaml.sh
|
||||
|
||||
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
|
||||
|
||||
.PHONY: ci-validate
|
||||
ci-validate:
|
||||
time make -B vendor
|
||||
time make -B compose-jsonschema
|
||||
time make manpages
|
||||
time make yamldocs
|
||||
@scripts/validate/check-git-diff cli/compose/schema/bindata.go
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
[](https://circleci.com/gh/docker/cli/tree/master) [](https://jenkins.dockerproject.org/job/docker/job/cli/job/master/)
|
||||
[](https://circleci.com/gh/docker/cli/tree/master)
|
||||
|
||||
docker/cli
|
||||
==========
|
||||
@ -29,12 +29,6 @@ Run all linting:
|
||||
$ make -f docker.Makefile lint
|
||||
```
|
||||
|
||||
List all the available targets:
|
||||
|
||||
```
|
||||
$ make help
|
||||
```
|
||||
|
||||
### In-container development environment
|
||||
|
||||
Start an interactive development environment:
|
||||
|
||||
@ -1,87 +0,0 @@
|
||||
# Testing
|
||||
|
||||
The following guidelines summarize the testing policy for docker/cli.
|
||||
|
||||
## Unit Test Suite
|
||||
|
||||
All code changes should have unit test coverage.
|
||||
|
||||
Error cases should be tested with unit tests.
|
||||
|
||||
Bug fixes should be covered by new unit tests or additional assertions in
|
||||
existing unit tests.
|
||||
|
||||
### Details
|
||||
|
||||
The unit test suite follows the standard Go testing convention. Tests are
|
||||
located in the package directory in `_test.go` files.
|
||||
|
||||
Unit tests should be named using the convention:
|
||||
|
||||
```
|
||||
Test<Function Name><Test Case Name>
|
||||
```
|
||||
|
||||
[Table tests](https://github.com/golang/go/wiki/TableDrivenTests) should be used
|
||||
where appropriate, but may not be appropriate in all cases.
|
||||
|
||||
Assertions should be made using
|
||||
[testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and test
|
||||
requirements should be verified using
|
||||
[testify/require](https://godoc.org/github.com/stretchr/testify/require).
|
||||
|
||||
Fakes, and testing utilities can be found in
|
||||
[internal/test](https://godoc.org/github.com/docker/cli/internal/test) and
|
||||
[gotestyourself](https://godoc.org/github.com/gotestyourself/gotestyourself).
|
||||
|
||||
## End-to-End Test Suite
|
||||
|
||||
The end-to-end test suite tests a cli binary against a real API backend.
|
||||
|
||||
### Guidelines
|
||||
|
||||
Each feature (subcommand) should have a single end-to-end test for
|
||||
the success case. The test should include all (or most) flags/options supported
|
||||
by that feature.
|
||||
|
||||
In some rare cases a couple additional end-to-end tests may be written for a
|
||||
sufficiently complex and critical feature (ex: `container run`, `service
|
||||
create`, `service update`, and `docker build` may have ~3-5 cases each).
|
||||
|
||||
In some rare cases a sufficiently critical error paths may have a single
|
||||
end-to-end test case.
|
||||
|
||||
In all other cases the behaviour should be covered by unit tests.
|
||||
|
||||
If a code change adds a new flag, that flag should be added to the existing
|
||||
"success case" end-to-end test.
|
||||
|
||||
If a code change fixes a bug, that bug fix should be covered either by adding
|
||||
assertions to the existing end-to-end test, or with one or more unit test.
|
||||
|
||||
### Details
|
||||
|
||||
The end-to-end test suite is located in
|
||||
[./e2e](https://github.com/docker/cli/tree/master/e2e). Each directory in `e2e`
|
||||
corresponds to a directory in `cli/command` and contains the tests for that
|
||||
subcommand. Files in each directory should be named `<command>_test.go` where
|
||||
command is the basename of the command (ex: the test for `docker stack deploy`
|
||||
is found in `e2e/stack/deploy_test.go`).
|
||||
|
||||
Tests should be named using the convention:
|
||||
|
||||
```
|
||||
Test<Command Basename>[<Test Case Name>]
|
||||
```
|
||||
|
||||
where the test case name is only required when there are multiple test cases for
|
||||
a single command.
|
||||
|
||||
End-to-end test should run the `docker` binary using
|
||||
[gotestyourself/icmd](https://godoc.org/github.com/gotestyourself/gotestyourself/icmd)
|
||||
and make assertions about the exit code, stdout, stderr, and local file system.
|
||||
|
||||
Any Docker image or registry operations should use `registry:5000/<image name>`
|
||||
to communicate with the local instance of the Docker registry. To load
|
||||
additional fixture images to the registry see
|
||||
[scripts/test/e2e/run](https://github.com/docker/cli/blob/master/scripts/test/e2e/run).
|
||||
@ -1 +1 @@
|
||||
17.12.1-ce
|
||||
17.06.2-ce
|
||||
|
||||
@ -1,116 +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 || \
|
||||
echo 'Codecov failed to upload'
|
||||
|
||||
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 ci-validate
|
||||
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 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
|
||||
|
||||
@ -17,7 +17,6 @@ func SetupRootCommand(rootCmd *cobra.Command) {
|
||||
cobra.AddTemplateFunc("operationSubCommands", operationSubCommands)
|
||||
cobra.AddTemplateFunc("managementSubCommands", managementSubCommands)
|
||||
cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages)
|
||||
cobra.AddTemplateFunc("useLine", UseLine)
|
||||
|
||||
rootCmd.SetUsageTemplate(usageTemplate)
|
||||
rootCmd.SetHelpTemplate(helpTemplate)
|
||||
@ -26,7 +25,6 @@ func SetupRootCommand(rootCmd *cobra.Command) {
|
||||
|
||||
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
|
||||
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
|
||||
rootCmd.PersistentFlags().Lookup("help").Hidden = true
|
||||
}
|
||||
|
||||
// FlagErrorFunc prints an error message which matches the format of the
|
||||
@ -99,19 +97,9 @@ func managementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
return cmds
|
||||
}
|
||||
|
||||
// UseLine returns the usage line for a command. This implementation is different
|
||||
// from the default Command.UseLine in that it does not add a `[flags]` to the
|
||||
// end of the line.
|
||||
func UseLine(cmd *cobra.Command) string {
|
||||
if cmd.HasParent() {
|
||||
return cmd.Parent().CommandPath() + " " + cmd.Use
|
||||
}
|
||||
return cmd.Use
|
||||
}
|
||||
|
||||
var usageTemplate = `Usage:
|
||||
|
||||
{{- if not .HasSubCommands}} {{ useLine . }}{{end}}
|
||||
{{- if not .HasSubCommands}} {{.UseLine}}{{end}}
|
||||
{{- if .HasSubCommands}} {{ .CommandPath}} COMMAND{{end}}
|
||||
|
||||
{{ .Short | trim }}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -9,11 +9,11 @@ import (
|
||||
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkpoint",
|
||||
Short: "Manage checkpoints",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"experimental": "", "version": "1.25"},
|
||||
Use: "checkpoint",
|
||||
Short: "Manage checkpoints",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Tags: map[string]string{"experimental": "", "version": "1.25"},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCli),
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
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,67 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/gotestyourself/gotestyourself/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.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: tc.checkpointListFunc,
|
||||
})
|
||||
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
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return []types.Checkpoint{
|
||||
{Name: "checkpoint-foo"},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
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)
|
||||
golden.Assert(t, cli.OutBuffer().String(), "checkpoint-list-with-options.golden")
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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 arguments",
|
||||
},
|
||||
{
|
||||
args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
},
|
||||
{
|
||||
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.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: tc.checkpointDeleteFunc,
|
||||
})
|
||||
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.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
return nil
|
||||
},
|
||||
})
|
||||
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,6 +1,7 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -11,18 +12,18 @@ import (
|
||||
"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"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
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"
|
||||
"github.com/docker/notary/passphrase"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/theupdateframework/notary"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
"github.com/theupdateframework/notary/passphrase"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -41,8 +42,7 @@ type Cli interface {
|
||||
In() *InStream
|
||||
SetIn(in *InStream)
|
||||
ConfigFile() *configfile.ConfigFile
|
||||
ServerInfo() ServerInfo
|
||||
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
CredentialsStore(serverAddress string) credentials.Store
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
@ -107,66 +107,106 @@ 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)
|
||||
if tlsconfig.IsErrEncryptedKey(err) {
|
||||
var (
|
||||
passwd string
|
||||
giveup bool
|
||||
)
|
||||
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
|
||||
newClient := func(password string) (client.APIClient, error) {
|
||||
opts.Common.TLSOptions.Passphrase = password
|
||||
return NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||
|
||||
for attempts := 0; tlsconfig.IsErrEncryptedKey(err); attempts++ {
|
||||
// some code and comments borrowed from notary/trustmanager/keystore.go
|
||||
passwd, giveup, err = passRetriever("private", "encrypted TLS private", false, attempts)
|
||||
// Check if the passphrase retriever got an error or if it is telling us to give up
|
||||
if giveup || err != nil {
|
||||
return errors.Wrap(err, "private key is encrypted, but could not get passphrase")
|
||||
}
|
||||
|
||||
opts.Common.TLSOptions.Passphrase = passwd
|
||||
cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
|
||||
}
|
||||
cli.client, err = getClientWithPassword(passRetriever, newClient)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.initializeFromClient()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) initializeFromClient() {
|
||||
cli.defaultVersion = cli.client.ClientVersion()
|
||||
|
||||
ping, err := cli.client.Ping(context.Background())
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.server = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cli.server = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
func getClientWithPassword(passRetriever notary.PassRetriever, newClient func(password string) (client.APIClient, error)) (client.APIClient, error) {
|
||||
for attempts := 0; ; attempts++ {
|
||||
passwd, giveup, err := passRetriever("private", "encrypted TLS private", false, attempts)
|
||||
if giveup || err != nil {
|
||||
return nil, errors.Wrap(err, "private key is encrypted, but could not get passphrase")
|
||||
if ping, err := cli.client.Ping(context.Background()); err == nil {
|
||||
cli.server = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
}
|
||||
|
||||
apiclient, err := newClient(passwd)
|
||||
if !tlsconfig.IsErrEncryptedKey(err) {
|
||||
return apiclient, err
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
|
||||
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
|
||||
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
@ -181,6 +221,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)
|
||||
@ -207,8 +260,7 @@ func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.
|
||||
return client.NewClient(host, verStr, httpClient, customHeaders)
|
||||
}
|
||||
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
|
||||
var host string
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) {
|
||||
switch len(hosts) {
|
||||
case 0:
|
||||
host = os.Getenv("DOCKER_HOST")
|
||||
@ -218,7 +270,8 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
|
||||
return "", errors.New("Please specify only one -H")
|
||||
}
|
||||
|
||||
return dopts.ParseHost(tlsOptions != nil, host)
|
||||
host, err = dopts.ParseHost(tlsOptions != nil, host)
|
||||
return
|
||||
}
|
||||
|
||||
func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, error) {
|
||||
|
||||
@ -1,198 +0,0 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"crypto/x509"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
host := "unix://path"
|
||||
opts := &flags.CommonOptions{Hosts: []string{host}}
|
||||
configFile := &configfile.ConfigFile{
|
||||
HTTPHeaders: map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
},
|
||||
}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, host, apiclient.DaemonHost())
|
||||
|
||||
expectedHeaders := map[string]string{
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
assert.Equal(t, expectedHeaders, apiclient.(*client.Client).CustomHTTPHeaders())
|
||||
assert.Equal(t, api.DefaultVersion, apiclient.ClientVersion())
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
customVersion := "v3.3.3"
|
||||
defer patchEnvVariable(t, "DOCKER_API_VERSION", customVersion)()
|
||||
|
||||
opts := &flags.CommonOptions{}
|
||||
configFile := &configfile.ConfigFile{}
|
||||
apiclient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, customVersion, apiclient.ClientVersion())
|
||||
}
|
||||
|
||||
// TODO: use gotestyourself/env.Patch
|
||||
func patchEnvVariable(t *testing.T, key, value string) func() {
|
||||
oldValue, ok := os.LookupEnv(key)
|
||||
require.NoError(t, os.Setenv(key, value))
|
||||
return func() {
|
||||
if !ok {
|
||||
require.NoError(t, os.Unsetenv(key))
|
||||
return
|
||||
}
|
||||
require.NoError(t, os.Setenv(key, oldValue))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (types.Ping, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
}
|
||||
|
||||
func (c *fakeClient) ClientVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||
c.negotiated = true
|
||||
}
|
||||
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
defaultVersion := "v1.55"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
pingFunc func() (types.Ping, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
apiclient := &fakeClient{
|
||||
pingFunc: testcase.pingFunc,
|
||||
version: defaultVersion,
|
||||
}
|
||||
|
||||
cli := &DockerCli{client: apiclient}
|
||||
cli.initializeFromClient()
|
||||
assert.Equal(t, defaultVersion, cli.defaultVersion)
|
||||
assert.Equal(t, testcase.expectedServer, cli.server)
|
||||
assert.Equal(t, testcase.negotiated, apiclient.negotiated)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientWithPassword(t *testing.T) {
|
||||
expected := "password"
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
password string
|
||||
retrieverErr error
|
||||
retrieverGiveup bool
|
||||
newClientErr error
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful connect",
|
||||
password: expected,
|
||||
},
|
||||
{
|
||||
doc: "password retriever exhausted",
|
||||
retrieverGiveup: true,
|
||||
retrieverErr: errors.New("failed"),
|
||||
expectedErr: "private key is encrypted, but could not get passphrase",
|
||||
},
|
||||
{
|
||||
doc: "password retriever error",
|
||||
retrieverErr: errors.New("failed"),
|
||||
expectedErr: "failed",
|
||||
},
|
||||
{
|
||||
doc: "newClient error",
|
||||
newClientErr: errors.New("failed to connect"),
|
||||
expectedErr: "failed to connect",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
passRetriever := func(_, _ string, _ bool, attempts int) (passphrase string, giveup bool, err error) {
|
||||
// Always return an invalid pass first to test iteration
|
||||
switch attempts {
|
||||
case 0:
|
||||
return "something else", false, nil
|
||||
default:
|
||||
return testcase.password, testcase.retrieverGiveup, testcase.retrieverErr
|
||||
}
|
||||
}
|
||||
|
||||
newClient := func(currentPassword string) (client.APIClient, error) {
|
||||
if testcase.newClientErr != nil {
|
||||
return nil, testcase.newClientErr
|
||||
}
|
||||
if currentPassword == expected {
|
||||
return &client.Client{}, nil
|
||||
}
|
||||
return &client.Client{}, x509.IncorrectPasswordError
|
||||
}
|
||||
|
||||
_, err := getClientWithPassword(passRetriever, newClient)
|
||||
if testcase.expectedErr != "" {
|
||||
testutil.ErrorContains(t, err, testcase.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,6 @@ import (
|
||||
"github.com/docker/cli/cli/command/stack"
|
||||
"github.com/docker/cli/cli/command/swarm"
|
||||
"github.com/docker/cli/cli/command/system"
|
||||
"github.com/docker/cli/cli/command/trust"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -70,9 +69,6 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
|
||||
// swarm
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// trust
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
|
||||
// volume
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
|
||||
@ -8,13 +8,14 @@ import (
|
||||
)
|
||||
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// nolint: interfacer
|
||||
func NewConfigCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Annotations: map[string]string{"version": "1.30"},
|
||||
Use: "config",
|
||||
Short: "Manage Docker configs",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
Tags: map[string]string{"version": "1.30"},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCli),
|
||||
|
||||
@ -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,17 +1,18 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -26,10 +27,10 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
args: []string{"too_few"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{args: []string{"too", "many", "arguments"},
|
||||
expectedError: "requires exactly 2 arguments",
|
||||
expectedError: "requires exactly 2 argument(s)",
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
@ -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,13 +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())
|
||||
golden.Assert(t, string(actual), configDataFile)
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
|
||||
expected := golden.Get(t, actual, configDataFile)
|
||||
assert.Equal(t, string(expected), string(actual))
|
||||
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
|
||||
}
|
||||
|
||||
func TestConfigCreateWithLabels(t *testing.T) {
|
||||
@ -82,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 {
|
||||
@ -96,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()))
|
||||
}
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -52,10 +53,11 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}),
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
for key, value := range tc.flags {
|
||||
@ -93,11 +95,17 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{configInspectFunc: tc.configInspectFunc})
|
||||
cmd := newConfigInspectCommand(cli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-without-format.%s.golden", tc.name))
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,14 +135,18 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
})
|
||||
cmd := newConfigInspectCommand(cli)
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.Flags().Set("format", tc.format)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-with-format.%s.golden", tc.name))
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,14 +172,16 @@ func TestConfigInspectPretty(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
})
|
||||
cmd := newConfigInspectCommand(cli)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigInspectCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configInspectFunc: tc.configInspectFunc,
|
||||
}, buf))
|
||||
cmd.SetArgs([]string{"configID"})
|
||||
cmd.Flags().Set("pretty", "true")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), fmt.Sprintf("config-inspect-pretty.%s.golden", tc.name))
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,15 @@
|
||||
package config
|
||||
|
||||
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 byConfigName []swarm.Config
|
||||
|
||||
func (r byConfigName) Len() int { return len(r) }
|
||||
func (r byConfigName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||
func (r byConfigName) Less(i, j int) bool {
|
||||
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
|
||||
}
|
||||
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
format string
|
||||
@ -67,8 +55,6 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(byConfigName(configs))
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewConfigFormat(format, options.quiet),
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
// Import builders to get the builder function as package function
|
||||
. "github.com/docker/cli/internal/test/builders"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
. "github.com/docker/cli/cli/internal/test/builders"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/pkg/testutil/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -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,36 +49,36 @@ 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{
|
||||
*Config(ConfigID("ID-1-foo"),
|
||||
ConfigName("1-foo"),
|
||||
*Config(ConfigID("ID-foo"),
|
||||
ConfigName("foo"),
|
||||
ConfigVersion(swarm.Version{Index: 10}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-10-foo"),
|
||||
ConfigName("10-foo"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*Config(ConfigID("ID-2-foo"),
|
||||
ConfigName("2-foo"),
|
||||
*Config(ConfigID("ID-bar"),
|
||||
ConfigName("bar"),
|
||||
ConfigVersion(swarm.Version{Index: 11}),
|
||||
ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cli.SetConfigfile(&configfile.ConfigFile{})
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.SetOutput(buf)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-sort.golden")
|
||||
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{
|
||||
@ -86,14 +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())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-quiet-option.golden")
|
||||
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{
|
||||
@ -103,16 +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())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-config-format.golden")
|
||||
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{
|
||||
@ -122,14 +131,17 @@ func TestConfigListWithFormat(t *testing.T) {
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cmd := newConfigListCommand(cli)
|
||||
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
|
||||
assert.NoError(t, cmd.Execute())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-format.golden")
|
||||
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])
|
||||
@ -149,10 +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())
|
||||
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-filter.golden")
|
||||
actual := buf.String()
|
||||
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
|
||||
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/cli/cli/internal/test"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -19,7 +20,7 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
expectedError: "requires at least 1 argument.",
|
||||
expectedError: "requires at least 1 argument(s).",
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
@ -30,10 +31,11 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
buf := new(bytes.Buffer)
|
||||
cmd := newConfigRemoveCommand(
|
||||
test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: tc.configRemoveFunc,
|
||||
}),
|
||||
}, buf),
|
||||
)
|
||||
cmd.SetArgs(tc.args)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
@ -43,22 +45,24 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
|
||||
func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
buf := new(bytes.Buffer)
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
assert.NoError(t, cmd.Execute())
|
||||
assert.Equal(t, names, strings.Split(strings.TrimSpace(cli.OutBuffer().String()), "\n"))
|
||||
assert.Equal(t, names, strings.Split(strings.TrimSpace(buf.String()), "\n"))
|
||||
assert.Equal(t, names, removedConfigs)
|
||||
}
|
||||
|
||||
func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
buf := new(bytes.Buffer)
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
@ -69,11 +73,10 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}, buf)
|
||||
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
cmd.SetArgs(names)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
assert.EqualError(t, cmd.Execute(), "error removing config: foo")
|
||||
assert.Equal(t, names, removedConfigs)
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
ID: configID
|
||||
Name: configName
|
||||
ID: configID
|
||||
Name: configName
|
||||
Labels:
|
||||
- lbl1=value1
|
||||
Created at: 0001-01-01 00:00:00 +0000 utc
|
||||
Updated at: 0001-01-01 00:00:00 +0000 utc
|
||||
- lbl1=value1
|
||||
Created at: 0001-01-01 00:00:00+0000 utc
|
||||
Updated at: 0001-01-01 00:00:00+0000 utc
|
||||
Data:
|
||||
payload here
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"ID": "ID-foo",
|
||||
"Version": {},
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
@ -13,7 +13,7 @@
|
||||
},
|
||||
{
|
||||
"ID": "ID-bar",
|
||||
"Version": {},
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"ID": "ID-foo",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Version": {},
|
||||
"CreatedAt": "0001-01-01T00:00:00Z",
|
||||
"UpdatedAt": "0001-01-01T00:00:00Z",
|
||||
"Spec": {
|
||||
"Name": "foo",
|
||||
"Labels": null
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-1-foo 1-foo 2 hours ago About an hour ago
|
||||
ID-2-foo 2-foo 2 hours ago About an hour ago
|
||||
ID-10-foo 10-foo 2 hours ago About an hour ago
|
||||
@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
foo
|
||||
bar label=label-bar
|
||||
foo
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
ID-bar
|
||||
ID-foo
|
||||
ID-bar
|
||||
|
||||
3
components/cli/cli/command/config/testdata/config-list.golden
vendored
Normal file
3
components/cli/cli/command/config/testdata/config-list.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
ID NAME CREATED UPDATED
|
||||
ID-foo foo 2 hours ago About an hour ago
|
||||
ID-bar bar 2 hours ago About an hour ago
|
||||
@ -4,13 +4,12 @@ import (
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -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,21 +95,19 @@ 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() {
|
||||
resizeTTY(ctx, dockerCli, opts.container)
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTtyTo(ctx, client, opts.container, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil {
|
||||
logrus.Debugf("Error monitoring TTY size: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
streamer := hijackedIOStreamer{
|
||||
@ -140,36 +127,14 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
return getExitStatus(ctx, dockerCli.Client(), opts.container)
|
||||
}
|
||||
|
||||
func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
// To handle the case where a user repeatedly attaches/detaches without resizing their
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
if err := MonitorTtySize(ctx, dockerCli, containerID, false); err != nil {
|
||||
logrus.Debugf("Error monitoring TTY size: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getExitStatus(ctx context.Context, apiclient client.ContainerAPIClient, containerID string) error {
|
||||
container, err := apiclient.ContainerInspect(ctx, containerID)
|
||||
_, status, err := getExitCode(ctx, dockerCli, opts.container)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
return err
|
||||
}
|
||||
return cli.StatusError{StatusCode: -1}
|
||||
return err
|
||||
}
|
||||
status := container.State.ExitCode
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,117 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestNewAttachCommandErrors(t *testing.T) {
|
||||
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{inspectFunc: tc.containerInspectFunc}))
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExitStatus(t *testing.T) {
|
||||
containerID := "the exec id"
|
||||
expecatedErr := errors.New("unexpected error")
|
||||
|
||||
testcases := []struct {
|
||||
inspectError error
|
||||
exitCode int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
inspectError: nil,
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
inspectError: expecatedErr,
|
||||
expectedError: expecatedErr,
|
||||
},
|
||||
{
|
||||
exitCode: 15,
|
||||
expectedError: cli.StatusError{StatusCode: 15},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
client := &fakeClient{
|
||||
inspectFunc: func(id string) (types.ContainerJSON, error) {
|
||||
assert.Equal(t, containerID, id)
|
||||
return types.ContainerJSON{
|
||||
ContainerJSONBase: &types.ContainerJSONBase{
|
||||
State: &types.ContainerState{ExitCode: testcase.exitCode},
|
||||
},
|
||||
}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
err := getExitStatus(context.Background(), client, containerID)
|
||||
assert.Equal(t, testcase.expectedError, err)
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (types.ContainerJSON, error)
|
||||
execInspectFunc func(execID string) (types.ContainerExecInspect, error)
|
||||
execCreateFunc func(container string, config types.ExecConfig) (types.IDResponse, error)
|
||||
createContainerFunc func(config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (container.ContainerCreateCreatedBody, error)
|
||||
imageCreateFunc func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (types.Info, error)
|
||||
containerStatPathFunc func(container, path string) (types.ContainerPathStat, error)
|
||||
containerCopyFromFunc func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error)
|
||||
logFunc func(string, types.ContainerLogsOptions) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
|
||||
if f.inspectFunc != nil {
|
||||
return f.inspectFunc(containerID)
|
||||
}
|
||||
return types.ContainerJSON{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
|
||||
if f.execCreateFunc != nil {
|
||||
return f.execCreateFunc(container, config)
|
||||
}
|
||||
return types.IDResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
|
||||
if f.execInspectFunc != nil {
|
||||
return f.execInspectFunc(execID)
|
||||
}
|
||||
return types.ContainerExecInspect{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCreate(
|
||||
_ context.Context,
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
if f.createContainerFunc != nil {
|
||||
return f.createContainerFunc(config, hostConfig, networkingConfig, containerName)
|
||||
}
|
||||
return container.ContainerCreateCreatedBody{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
|
||||
if f.imageCreateFunc != nil {
|
||||
return f.imageCreateFunc(parentReference, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
|
||||
if f.infoFunc != nil {
|
||||
return f.infoFunc()
|
||||
}
|
||||
return types.Info{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, container, path string) (types.ContainerPathStat, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(container, path)
|
||||
}
|
||||
return types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(container, srcPath)
|
||||
}
|
||||
return nil, types.ContainerPathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(container, options)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@ -7,7 +7,8 @@ import (
|
||||
)
|
||||
|
||||
// NewContainerCommand returns a cobra command for `container` subcommands
|
||||
func NewContainerCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// nolint: interfacer
|
||||
func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "container",
|
||||
Short: "Manage containers",
|
||||
|
||||
@ -22,7 +22,7 @@ type commitOptions struct {
|
||||
}
|
||||
|
||||
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||
func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewCommitCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var options commitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -51,7 +51,7 @@ func NewCommitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCommit(dockerCli command.Cli, options *commitOptions) error {
|
||||
func runCommit(dockerCli *command.DockerCli, options *commitOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
name := options.container
|
||||
|
||||
@ -26,21 +26,17 @@ type copyOptions struct {
|
||||
type copyDirection int
|
||||
|
||||
const (
|
||||
fromContainer copyDirection = 1 << iota
|
||||
fromContainer copyDirection = (1 << iota)
|
||||
toContainer
|
||||
acrossContainers = fromContainer | toContainer
|
||||
)
|
||||
|
||||
type cpConfig struct {
|
||||
followLink bool
|
||||
copyUIDGID bool
|
||||
sourcePath string
|
||||
destPath string
|
||||
container string
|
||||
}
|
||||
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts copyOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -69,57 +65,58 @@ func NewCopyCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCopy(dockerCli command.Cli, opts copyOptions) error {
|
||||
func runCopy(dockerCli *command.DockerCli, opts copyOptions) error {
|
||||
srcContainer, srcPath := splitCpArg(opts.source)
|
||||
destContainer, destPath := splitCpArg(opts.destination)
|
||||
|
||||
copyConfig := cpConfig{
|
||||
followLink: opts.followLink,
|
||||
copyUIDGID: opts.copyUIDGID,
|
||||
sourcePath: srcPath,
|
||||
destPath: destPath,
|
||||
}
|
||||
dstContainer, dstPath := splitCpArg(opts.destination)
|
||||
|
||||
var direction copyDirection
|
||||
if srcContainer != "" {
|
||||
direction |= fromContainer
|
||||
copyConfig.container = srcContainer
|
||||
}
|
||||
if destContainer != "" {
|
||||
if dstContainer != "" {
|
||||
direction |= toContainer
|
||||
copyConfig.container = destContainer
|
||||
}
|
||||
|
||||
cpParam := &cpConfig{
|
||||
followLink: opts.followLink,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
switch direction {
|
||||
case fromContainer:
|
||||
return copyFromContainer(ctx, dockerCli, copyConfig)
|
||||
return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam)
|
||||
case toContainer:
|
||||
return copyToContainer(ctx, dockerCli, copyConfig)
|
||||
return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam, opts.copyUIDGID)
|
||||
case acrossContainers:
|
||||
// Copying between containers isn't supported.
|
||||
return errors.New("copying between containers is not supported")
|
||||
default:
|
||||
// User didn't specify any container.
|
||||
return errors.New("must specify at least one container source")
|
||||
}
|
||||
}
|
||||
|
||||
func statContainerPath(ctx context.Context, dockerCli *command.DockerCli, containerName, path string) (types.ContainerPathStat, error) {
|
||||
return dockerCli.Client().ContainerStatPath(ctx, containerName, path)
|
||||
}
|
||||
|
||||
func resolveLocalPath(localPath string) (absPath string, err error) {
|
||||
if absPath, err = filepath.Abs(localPath); err != nil {
|
||||
return
|
||||
}
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
|
||||
|
||||
return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
|
||||
}
|
||||
|
||||
func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||
dstPath := copyConfig.destPath
|
||||
srcPath := copyConfig.sourcePath
|
||||
|
||||
func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
|
||||
if dstPath != "-" {
|
||||
// Get an absolute destination path.
|
||||
dstPath, err = resolveLocalPath(dstPath)
|
||||
@ -128,11 +125,10 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
|
||||
}
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
if copyConfig.followLink {
|
||||
srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||
if cpParam.followLink {
|
||||
srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath)
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
@ -149,17 +145,20 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
|
||||
|
||||
}
|
||||
|
||||
content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
_, err = io.Copy(dockerCli.Out(), content)
|
||||
// Send the response to STDOUT.
|
||||
_, err = io.Copy(os.Stdout, content)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare source copy info.
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
@ -172,17 +171,13 @@ func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cp
|
||||
_, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
|
||||
preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
|
||||
}
|
||||
// See comments in the implementation of `archive.CopyTo` for exactly what
|
||||
// goes into deciding how and whether the source archive needs to be
|
||||
// altered for the correct copy behavior.
|
||||
return archive.CopyTo(preArchive, srcInfo, dstPath)
|
||||
}
|
||||
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) {
|
||||
srcPath := copyConfig.sourcePath
|
||||
dstPath := copyConfig.destPath
|
||||
|
||||
func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig, copyUIDGID bool) (err error) {
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
@ -191,10 +186,14 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
|
||||
}
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
// In order to get the copy behavior right, we need to know information
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||
dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath)
|
||||
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
@ -206,7 +205,7 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
|
||||
}
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
@ -225,14 +224,15 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
|
||||
)
|
||||
|
||||
if srcPath == "-" {
|
||||
// Use STDIN.
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
if !dstInfo.IsDir {
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath)
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink)
|
||||
srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -267,9 +267,10 @@ func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpCo
|
||||
|
||||
options := types.CopyToContainerOptions{
|
||||
AllowOverwriteDirWithFile: false,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
CopyUIDGID: copyUIDGID,
|
||||
}
|
||||
return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
|
||||
return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options)
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
|
||||
@ -1,160 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/gotestyourself/gotestyourself/skip"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRunCopyWithInvalidArguments(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options copyOptions
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "copy between container",
|
||||
options: copyOptions{
|
||||
source: "first:/path",
|
||||
destination: "second:/path",
|
||||
},
|
||||
expectedErr: "copying between containers is not supported",
|
||||
},
|
||||
{
|
||||
doc: "copy without a container",
|
||||
options: copyOptions{
|
||||
source: "./source",
|
||||
destination: "./dest",
|
||||
},
|
||||
expectedErr: "must specify at least one container source",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
err := runCopy(test.NewFakeCli(nil), testcase.options)
|
||||
assert.EqualError(t, err, testcase.expectedErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Equal(t, "container", container)
|
||||
return ioutil.NopCloser(strings.NewReader(tarContent)), types.ContainerPathStat{}, nil
|
||||
},
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: "-"}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tarContent, cli.OutBuffer().String())
|
||||
assert.Equal(t, "", cli.ErrBuffer().String())
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Equal(t, "container", container)
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
}
|
||||
options := copyOptions{source: "container:/path", destination: destDir.Path()}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "", cli.OutBuffer().String())
|
||||
assert.Equal(t, "", cli.ErrBuffer().String())
|
||||
|
||||
content, err := ioutil.ReadFile(destDir.Join("file1"))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "content\n", string(content))
|
||||
}
|
||||
|
||||
func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test",
|
||||
fs.WithFile("file1", "content\n"))
|
||||
defer destDir.Remove()
|
||||
|
||||
fakeClient := &fakeClient{
|
||||
containerCopyFromFunc: func(container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) {
|
||||
assert.Equal(t, "container", container)
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return readCloser, types.ContainerPathStat{}, err
|
||||
},
|
||||
}
|
||||
|
||||
options := copyOptions{
|
||||
source: "container:/path",
|
||||
destination: destDir.Join("missing", "foo"),
|
||||
}
|
||||
cli := test.NewFakeCli(fakeClient)
|
||||
err := runCopy(cli, options)
|
||||
testutil.ErrorContains(t, err, destDir.Join("missing"))
|
||||
}
|
||||
|
||||
func TestSplitCpArg(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
path string
|
||||
os string
|
||||
expectedContainer string
|
||||
expectedPath string
|
||||
}{
|
||||
{
|
||||
doc: "absolute path with colon",
|
||||
os: "linux",
|
||||
path: "/abs/path:withcolon",
|
||||
expectedPath: "/abs/path:withcolon",
|
||||
},
|
||||
{
|
||||
doc: "relative path with colon",
|
||||
path: "./relative:path",
|
||||
expectedPath: "./relative:path",
|
||||
},
|
||||
{
|
||||
doc: "absolute path with drive",
|
||||
os: "windows",
|
||||
path: `d:\abs\path`,
|
||||
expectedPath: `d:\abs\path`,
|
||||
},
|
||||
{
|
||||
doc: "no separator",
|
||||
path: "relative/path",
|
||||
expectedPath: "relative/path",
|
||||
},
|
||||
{
|
||||
doc: "with separator",
|
||||
path: "container:/opt/foo",
|
||||
expectedPath: "/opt/foo",
|
||||
expectedContainer: "container",
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
skip.IfCondition(t, testcase.os != "" && testcase.os != runtime.GOOS)
|
||||
|
||||
container, path := splitCpArg(testcase.path)
|
||||
assert.Equal(t, testcase.expectedContainer, container)
|
||||
assert.Equal(t, testcase.expectedPath, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,7 @@ import (
|
||||
)
|
||||
|
||||
type createOptions struct {
|
||||
name string
|
||||
platform string
|
||||
name string
|
||||
}
|
||||
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
@ -52,7 +51,6 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddPlatformFlag(flags, &opts.platform)
|
||||
command.AddTrustVerificationFlags(flags)
|
||||
copts = addFlags(flags)
|
||||
return cmd
|
||||
@ -64,7 +62,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
|
||||
reportError(dockerCli.Err(), "create", err.Error(), true)
|
||||
return cli.StatusError{StatusCode: 125}
|
||||
}
|
||||
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name, opts.platform)
|
||||
response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -72,7 +70,7 @@ func runCreate(dockerCli command.Cli, flags *pflag.FlagSet, opts *createOptions,
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, image string, platform string, out io.Writer) error {
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, image string, out io.Writer) error {
|
||||
ref, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -92,7 +90,6 @@ func pullImage(ctx context.Context, dockerCli command.Cli, image string, platfor
|
||||
|
||||
options := types.ImageCreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platform: platform,
|
||||
}
|
||||
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options)
|
||||
@ -116,9 +113,6 @@ type cidFile struct {
|
||||
}
|
||||
|
||||
func (cid *cidFile) Close() error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
cid.file.Close()
|
||||
|
||||
if cid.written {
|
||||
@ -132,9 +126,6 @@ func (cid *cidFile) Close() error {
|
||||
}
|
||||
|
||||
func (cid *cidFile) Write(id string) error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return errors.Errorf("Failed to write the container ID to the file: %s", err)
|
||||
}
|
||||
@ -143,9 +134,6 @@ func (cid *cidFile) Write(id string) error {
|
||||
}
|
||||
|
||||
func newCIDFile(path string) (*cidFile, error) {
|
||||
if path == "" {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path)
|
||||
}
|
||||
@ -158,22 +146,26 @@ func newCIDFile(path string) (*cidFile, error) {
|
||||
return &cidFile{path: path, file: f}, nil
|
||||
}
|
||||
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string, platform string) (*container.ContainerCreateCreatedBody, error) {
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) {
|
||||
config := containerConfig.Config
|
||||
hostConfig := containerConfig.HostConfig
|
||||
networkingConfig := containerConfig.NetworkingConfig
|
||||
stderr := dockerCli.Err()
|
||||
|
||||
var (
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
containerIDFile *cidFile
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
)
|
||||
|
||||
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cidfile := hostConfig.ContainerIDFile
|
||||
if cidfile != "" {
|
||||
var err error
|
||||
if containerIDFile, err = newCIDFile(cidfile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer containerIDFile.Close()
|
||||
}
|
||||
defer containerIDFile.Close()
|
||||
|
||||
ref, err := reference.ParseAnyReference(config.Image)
|
||||
if err != nil {
|
||||
@ -197,11 +189,11 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
|
||||
|
||||
//if image not found try to pull it
|
||||
if err != nil {
|
||||
if apiclient.IsErrNotFound(err) && namedRef != nil {
|
||||
if apiclient.IsErrImageNotFound(err) && namedRef != nil {
|
||||
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
if err := pullImage(ctx, dockerCli, config.Image, platform, stderr); err != nil {
|
||||
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
@ -223,6 +215,10 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
|
||||
for _, warning := range response.Warnings {
|
||||
fmt.Fprintf(stderr, "WARNING: %s\n", warning)
|
||||
}
|
||||
err = containerIDFile.Write(response.ID)
|
||||
return &response, err
|
||||
if containerIDFile != nil {
|
||||
if err = containerIDFile.Write(response.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/gotestyourself/gotestyourself/fs"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCIDFileNoOPWithNoFilename(t *testing.T) {
|
||||
file, err := newCIDFile("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, &cidFile{}, file)
|
||||
|
||||
assert.NoError(t, file.Write("id"))
|
||||
assert.NoError(t, file.Close())
|
||||
}
|
||||
|
||||
func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
|
||||
tempfile := fs.NewFile(t, "test-cid-file")
|
||||
defer tempfile.Remove()
|
||||
|
||||
_, err := newCIDFile(tempfile.Path())
|
||||
testutil.ErrorContains(t, err, "Container ID file found")
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithNoWrite(t *testing.T) {
|
||||
tempdir := fs.NewDir(t, "test-cid-file")
|
||||
defer tempdir.Remove()
|
||||
|
||||
path := tempdir.Join("cidfile")
|
||||
file, err := newCIDFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, file.path, path)
|
||||
|
||||
assert.NoError(t, file.Close())
|
||||
_, err = os.Stat(path)
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithWrite(t *testing.T) {
|
||||
tempdir := fs.NewDir(t, "test-cid-file")
|
||||
defer tempdir.Remove()
|
||||
|
||||
path := tempdir.Join("cidfile")
|
||||
file, err := newCIDFile(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
content := "id"
|
||||
assert.NoError(t, file.Write(content))
|
||||
|
||||
actual, err := ioutil.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, content, string(actual))
|
||||
|
||||
assert.NoError(t, file.Close())
|
||||
_, err = os.Stat(path)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCreateContainerPullsImageIfMissing(t *testing.T) {
|
||||
imageName := "does-not-exist-locally"
|
||||
responseCounter := 0
|
||||
containerID := "abcdef"
|
||||
|
||||
client := &fakeClient{
|
||||
createContainerFunc: func(
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
containerName string,
|
||||
) (container.ContainerCreateCreatedBody, error) {
|
||||
defer func() { responseCounter++ }()
|
||||
switch responseCounter {
|
||||
case 0:
|
||||
return container.ContainerCreateCreatedBody{}, fakeNotFound{}
|
||||
case 1:
|
||||
return container.ContainerCreateCreatedBody{ID: containerID}, nil
|
||||
default:
|
||||
return container.ContainerCreateCreatedBody{}, errors.New("unexpected")
|
||||
}
|
||||
},
|
||||
imageCreateFunc: func(parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
infoFunc: func() (types.Info, error) {
|
||||
return types.Info{IndexServerAddress: "http://indexserver"}, nil
|
||||
},
|
||||
}
|
||||
cli := test.NewFakeCli(client)
|
||||
config := &containerConfig{
|
||||
Config: &container.Config{
|
||||
Image: imageName,
|
||||
},
|
||||
HostConfig: &container.HostConfig{},
|
||||
}
|
||||
body, err := createContainer(context.Background(), cli, config, "name", runtime.GOOS)
|
||||
require.NoError(t, err)
|
||||
expected := container.ContainerCreateCreatedBody{ID: containerID}
|
||||
assert.Equal(t, expected, *body)
|
||||
stderr := cli.ErrBuffer().String()
|
||||
assert.Contains(t, stderr, "Unable to find image 'does-not-exist-locally:latest' locally")
|
||||
}
|
||||
|
||||
type fakeNotFound struct{}
|
||||
|
||||
func (f fakeNotFound) NotFound() bool { return true }
|
||||
func (f fakeNotFound) Error() string { return "error fake not found" }
|
||||
@ -14,7 +14,7 @@ type diffOptions struct {
|
||||
}
|
||||
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts diffOptions
|
||||
|
||||
return &cobra.Command{
|
||||
@ -28,7 +28,7 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(dockerCli command.Cli, opts *diffOptions) error {
|
||||
func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error {
|
||||
if opts.container == "" {
|
||||
return errors.New("Container name cannot be empty")
|
||||
}
|
||||
|
||||
@ -4,14 +4,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
apiclient "github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -23,18 +22,18 @@ type execOptions struct {
|
||||
detach bool
|
||||
user string
|
||||
privileged bool
|
||||
env opts.ListOpts
|
||||
workdir string
|
||||
container string
|
||||
command []string
|
||||
env *opts.ListOpts
|
||||
}
|
||||
|
||||
func newExecOptions() execOptions {
|
||||
return execOptions{env: opts.NewListOpts(opts.ValidateEnv)}
|
||||
func newExecOptions() *execOptions {
|
||||
var values []string
|
||||
return &execOptions{
|
||||
env: opts.NewListOptsRef(&values, opts.ValidateEnv),
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
@ -42,9 +41,9 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
Short: "Run a command in a running container",
|
||||
Args: cli.RequiresMinArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.container = args[0]
|
||||
options.command = args[1:]
|
||||
return runExec(dockerCli, options)
|
||||
container := args[0]
|
||||
execCmd := args[1:]
|
||||
return runExec(dockerCli, options, container, execCmd)
|
||||
},
|
||||
}
|
||||
|
||||
@ -57,16 +56,27 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags.BoolVarP(&options.detach, "detach", "d", false, "Detached mode: run command in the background")
|
||||
flags.StringVarP(&options.user, "user", "u", "", "Username or UID (format: <name|uid>[:<group|gid>])")
|
||||
flags.BoolVarP(&options.privileged, "privileged", "", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.env, "env", "e", "Set environment variables")
|
||||
flags.VarP(options.env, "env", "e", "Set environment variables")
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.StringVarP(&options.workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExec(dockerCli command.Cli, options execOptions) error {
|
||||
execConfig := parseExec(options, dockerCli.ConfigFile())
|
||||
// nolint: gocyclo
|
||||
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 {
|
||||
return cli.StatusError{StatusCode: 1}
|
||||
}
|
||||
|
||||
if options.detachKeys != "" {
|
||||
dockerCli.ConfigFile().DetachKeys = options.detachKeys
|
||||
}
|
||||
|
||||
// Send client escape keys
|
||||
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
|
||||
|
||||
ctx := context.Background()
|
||||
client := dockerCli.Client()
|
||||
|
||||
@ -74,7 +84,7 @@ func runExec(dockerCli command.Cli, options execOptions) error {
|
||||
// otherwise if we error out we will leak execIDs on the server (and
|
||||
// there's no easy way to clean those up). But also in order to make "not
|
||||
// exist" errors take precedence we do a dummy inspect first.
|
||||
if _, err := client.ContainerInspect(ctx, options.container); err != nil {
|
||||
if _, err := client.ContainerInspect(ctx, container); err != nil {
|
||||
return err
|
||||
}
|
||||
if !execConfig.Detach {
|
||||
@ -83,31 +93,37 @@ func runExec(dockerCli command.Cli, options execOptions) error {
|
||||
}
|
||||
}
|
||||
|
||||
response, err := client.ContainerExecCreate(ctx, options.container, *execConfig)
|
||||
response, err := client.ContainerExecCreate(ctx, container, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
execID := response.ID
|
||||
if execID == "" {
|
||||
return errors.New("exec ID empty")
|
||||
fmt.Fprintln(dockerCli.Out(), "exec ID empty")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Temp struct for execStart so that we don't need to transfer all the execConfig.
|
||||
if execConfig.Detach {
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Detach: execConfig.Detach,
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
return client.ContainerExecStart(ctx, execID, execStartCheck)
|
||||
}
|
||||
return interactiveExec(ctx, dockerCli, execConfig, execID)
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *types.ExecConfig, execID string) error {
|
||||
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.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
in io.ReadCloser
|
||||
errCh chan error
|
||||
)
|
||||
|
||||
if execConfig.AttachStdin {
|
||||
@ -124,34 +140,24 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
|
||||
}
|
||||
}
|
||||
|
||||
client := dockerCli.Client()
|
||||
execStartCheck := types.ExecStartCheck{
|
||||
Tty: execConfig.Tty,
|
||||
}
|
||||
resp, err := client.ContainerExecAttach(ctx, execID, execStartCheck)
|
||||
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Close()
|
||||
errCh = promise.Go(func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
tty: execConfig.Tty,
|
||||
detachKeys: execConfig.DetachKeys,
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
errCh <- func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp,
|
||||
tty: execConfig.Tty,
|
||||
detachKeys: execConfig.DetachKeys,
|
||||
}
|
||||
|
||||
return streamer.stream(ctx)
|
||||
}()
|
||||
}()
|
||||
return streamer.stream(ctx)
|
||||
})
|
||||
|
||||
if execConfig.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||
@ -164,36 +170,42 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
|
||||
return err
|
||||
}
|
||||
|
||||
return getExecExitStatus(ctx, client, execID)
|
||||
var status int
|
||||
if _, status, err = getExecExitCode(ctx, client, execID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getExecExitStatus(ctx context.Context, client apiclient.ContainerAPIClient, execID string) error {
|
||||
// getExecExitCode perform an inspect on the exec command. It returns
|
||||
// the running state and the exit code.
|
||||
func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) {
|
||||
resp, err := client.ContainerExecInspect(ctx, execID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !apiclient.IsErrConnectionFailed(err) {
|
||||
return err
|
||||
return false, -1, err
|
||||
}
|
||||
return cli.StatusError{StatusCode: -1}
|
||||
return false, -1, nil
|
||||
}
|
||||
status := resp.ExitCode
|
||||
if status != 0 {
|
||||
return cli.StatusError{StatusCode: status}
|
||||
}
|
||||
return nil
|
||||
|
||||
return resp.Running, resp.ExitCode, nil
|
||||
}
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecConfig {
|
||||
func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) {
|
||||
execConfig := &types.ExecConfig{
|
||||
User: opts.user,
|
||||
Privileged: opts.privileged,
|
||||
Tty: opts.tty,
|
||||
Cmd: opts.command,
|
||||
Cmd: execCmd,
|
||||
Detach: opts.detach,
|
||||
Env: opts.env.GetAll(),
|
||||
WorkingDir: opts.workdir,
|
||||
}
|
||||
|
||||
// If -d is not set, attach to everything by default
|
||||
@ -205,10 +217,9 @@ func parseExec(opts execOptions, configFile *configfile.ConfigFile) *types.ExecC
|
||||
}
|
||||
}
|
||||
|
||||
if opts.detachKeys != "" {
|
||||
execConfig.DetachKeys = opts.detachKeys
|
||||
} else {
|
||||
execConfig.DetachKeys = configFile.DetachKeys
|
||||
if opts.env != nil {
|
||||
execConfig.Env = opts.env.GetAll()
|
||||
}
|
||||
return execConfig
|
||||
|
||||
return execConfig, nil
|
||||
}
|
||||
|
||||
@ -1,227 +1,116 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func withDefaultOpts(options execOptions) execOptions {
|
||||
options.env = opts.NewListOpts(opts.ValidateEnv)
|
||||
if len(options.command) == 0 {
|
||||
options.command = []string{"command"}
|
||||
}
|
||||
return options
|
||||
type arguments struct {
|
||||
options execOptions
|
||||
execCmd []string
|
||||
}
|
||||
|
||||
func TestParseExec(t *testing.T) {
|
||||
testcases := []struct {
|
||||
options execOptions
|
||||
configFile configfile.ConfigFile
|
||||
expected types.ExecConfig
|
||||
}{
|
||||
valids := map[*arguments]*types.ExecConfig{
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
options: withDefaultOpts(execOptions{}),
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
{
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
options: withDefaultOpts(execOptions{
|
||||
command: []string{"command1", "command2"},
|
||||
}),
|
||||
execCmd: []string{"command1", "command2"},
|
||||
}: {
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
options: execOptions{
|
||||
interactive: true,
|
||||
tty: true,
|
||||
user: "uid",
|
||||
}),
|
||||
expected: types.ExecConfig{
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{detach: true}),
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
options: execOptions{
|
||||
detach: true,
|
||||
},
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
Detach: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
options: execOptions{
|
||||
tty: true,
|
||||
interactive: true,
|
||||
detach: true,
|
||||
}),
|
||||
expected: types.ExecConfig{
|
||||
Detach: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "de",
|
||||
Detach: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(execOptions{
|
||||
detach: true,
|
||||
detachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: types.ExecConfig{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ab",
|
||||
Detach: true,
|
||||
},
|
||||
execCmd: []string{"command"},
|
||||
}: {
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
Detach: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
execConfig := parseExec(testcase.options, &testcase.configFile)
|
||||
assert.Equal(t, testcase.expected, *execConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunExec(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options execOptions
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful detach",
|
||||
options: withDefaultOpts(execOptions{
|
||||
container: "thecontainer",
|
||||
detach: true,
|
||||
}),
|
||||
client: fakeClient{execCreateFunc: execCreateWithID},
|
||||
},
|
||||
{
|
||||
doc: "inspect error",
|
||||
options: newExecOptions(),
|
||||
client: fakeClient{
|
||||
inspectFunc: func(string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{}, errors.New("failed inspect")
|
||||
},
|
||||
},
|
||||
expectedError: "failed inspect",
|
||||
},
|
||||
{
|
||||
doc: "missing exec ID",
|
||||
options: newExecOptions(),
|
||||
expectedError: "exec ID empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := runExec(cli, testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
testutil.ErrorContains(t, err, testcase.expectedError)
|
||||
} else {
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
|
||||
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func execCreateWithID(_ string, _ types.ExecConfig) (types.IDResponse, error) {
|
||||
return types.IDResponse{ID: "execid"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
execID := "the exec id"
|
||||
expecatedErr := errors.New("unexpected error")
|
||||
|
||||
testcases := []struct {
|
||||
inspectError error
|
||||
exitCode int
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
inspectError: nil,
|
||||
exitCode: 0,
|
||||
},
|
||||
{
|
||||
inspectError: expecatedErr,
|
||||
expectedError: expecatedErr,
|
||||
},
|
||||
{
|
||||
exitCode: 15,
|
||||
expectedError: cli.StatusError{StatusCode: 15},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
client := &fakeClient{
|
||||
execInspectFunc: func(id string) (types.ContainerExecInspect, error) {
|
||||
assert.Equal(t, execID, id)
|
||||
return types.ContainerExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
},
|
||||
for valid, expectedExecConfig := range valids {
|
||||
execConfig, err := parseExec(&valid.options, valid.execCmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !compareExecConfig(expectedExecConfig, execConfig) {
|
||||
t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig)
|
||||
}
|
||||
err := getExecExitStatus(context.Background(), client, execID)
|
||||
assert.Equal(t, testcase.expectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
},
|
||||
},
|
||||
func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
|
||||
if config1.AttachStderr != config2.AttachStderr {
|
||||
return false
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
|
||||
cmd := NewExecCommand(cli)
|
||||
cmd.SetOutput(ioutil.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
|
||||
if config1.AttachStdin != config2.AttachStdin {
|
||||
return false
|
||||
}
|
||||
if config1.AttachStdout != config2.AttachStdout {
|
||||
return false
|
||||
}
|
||||
if config1.Detach != config2.Detach {
|
||||
return false
|
||||
}
|
||||
if config1.Privileged != config2.Privileged {
|
||||
return false
|
||||
}
|
||||
if config1.Tty != config2.Tty {
|
||||
return false
|
||||
}
|
||||
if config1.User != config2.User {
|
||||
return false
|
||||
}
|
||||
if len(config1.Cmd) != len(config2.Cmd) {
|
||||
return false
|
||||
}
|
||||
for index, value := range config1.Cmd {
|
||||
if value != config2.Cmd[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ type exportOptions struct {
|
||||
}
|
||||
|
||||
// NewExportCommand creates a new `docker export` command
|
||||
func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewExportCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts exportOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -36,7 +36,7 @@ func NewExportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runExport(dockerCli command.Cli, opts exportOptions) error {
|
||||
func runExport(dockerCli *command.DockerCli, opts exportOptions) error {
|
||||
if opts.output == "" && dockerCli.Out().IsTerminal() {
|
||||
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
|
||||
}
|
||||
|
||||
@ -6,12 +6,12 @@ import (
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/docker/docker/pkg/stdcopy"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -185,7 +185,6 @@ func setRawTerminal(streams command.Streams) error {
|
||||
return streams.Out().SetRawTerminal()
|
||||
}
|
||||
|
||||
// nolint: unparam
|
||||
func restoreTerminal(streams command.Streams, in io.Closer) error {
|
||||
streams.In().RestoreTerminal()
|
||||
streams.Out().RestoreTerminal()
|
||||
|
||||
@ -15,7 +15,7 @@ type inspectOptions struct {
|
||||
}
|
||||
|
||||
// newInspectCommand creates a new cobra.Command for `docker container inspect`
|
||||
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts inspectOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -35,7 +35,7 @@ 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()
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ type killOptions struct {
|
||||
}
|
||||
|
||||
// NewKillCommand creates a new cobra.Command for `docker kill`
|
||||
func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts killOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -36,7 +36,7 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runKill(dockerCli command.Cli, opts *killOptions) error {
|
||||
func runKill(dockerCli *command.DockerCli, opts *killOptions) error {
|
||||
var errs []string
|
||||
ctx := context.Background()
|
||||
errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error {
|
||||
|
||||
@ -7,8 +7,8 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/templates"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -25,7 +25,7 @@ type psOptions struct {
|
||||
}
|
||||
|
||||
// NewPsCommand creates a new cobra.Command for `docker ps`
|
||||
func NewPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
options := psOptions{filter: opts.NewFilterOpt()}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -51,7 +51,7 @@ func NewPsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
cmd := *NewPsCommand(dockerCli)
|
||||
cmd.Aliases = []string{"ps", "list"}
|
||||
cmd.Use = "ls [OPTIONS]"
|
||||
@ -109,7 +109,7 @@ func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, er
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func runPs(dockerCli command.Cli, options *psOptions) error {
|
||||
func runPs(dockerCli *command.DockerCli, options *psOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
listOptions, err := buildContainerListOptions(options)
|
||||
|
||||
@ -14,7 +14,6 @@ import (
|
||||
type logsOptions struct {
|
||||
follow bool
|
||||
since string
|
||||
until string
|
||||
timestamps bool
|
||||
details bool
|
||||
tail string
|
||||
@ -23,7 +22,7 @@ type logsOptions struct {
|
||||
}
|
||||
|
||||
// NewLogsCommand creates a new cobra.Command for `docker logs`
|
||||
func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts logsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -39,22 +38,19 @@ func NewLogsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output")
|
||||
flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.StringVar(&opts.until, "until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)")
|
||||
flags.SetAnnotation("until", "version", []string{"1.35"})
|
||||
flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps")
|
||||
flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs")
|
||||
flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runLogs(dockerCli command.Cli, opts *logsOptions) error {
|
||||
func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
options := types.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Since: opts.since,
|
||||
Until: opts.until,
|
||||
Timestamps: opts.timestamps,
|
||||
Follow: opts.follow,
|
||||
Tail: opts.tail,
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var logFn = func(expectedOut string) func(string, types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
return func(container string, opts types.ContainerLogsOptions) (io.ReadCloser, error) {
|
||||
return ioutil.NopCloser(strings.NewReader(expectedOut)), nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunLogs(t *testing.T) {
|
||||
inspectFn := func(containerID string) (types.ContainerJSON, error) {
|
||||
return types.ContainerJSON{
|
||||
Config: &container.Config{Tty: true},
|
||||
ContainerJSONBase: &types.ContainerJSONBase{State: &types.ContainerState{Running: false}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var testcases = []struct {
|
||||
doc string
|
||||
options *logsOptions
|
||||
client fakeClient
|
||||
expectedError string
|
||||
expectedOut string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
doc: "successful logs",
|
||||
expectedOut: "foo",
|
||||
options: &logsOptions{},
|
||||
client: fakeClient{logFunc: logFn("foo"), inspectFunc: inspectFn},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&testcase.client)
|
||||
|
||||
err := runLogs(cli, testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
testutil.ErrorContains(t, err, testcase.expectedError)
|
||||
} else {
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
}
|
||||
assert.Equal(t, testcase.expectedOut, cli.OutBuffer().String())
|
||||
assert.Equal(t, testcase.expectedErr, cli.ErrBuffer().String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -11,20 +11,20 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/Sirupsen/logrus"
|
||||
"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/sirupsen/logrus"
|
||||
"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
|
||||
@ -274,7 +274,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
|
||||
|
||||
// Low-level execution (cgroups, namespaces, ...)
|
||||
flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
|
||||
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC mode to use")
|
||||
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use")
|
||||
flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology")
|
||||
flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use")
|
||||
flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm")
|
||||
@ -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,17 +409,22 @@ 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
|
||||
}
|
||||
|
||||
ipcMode := container.IpcMode(copts.ipcMode)
|
||||
if !ipcMode.Valid() {
|
||||
return nil, errors.Errorf("--ipc: invalid IPC mode")
|
||||
}
|
||||
|
||||
pidMode := container.PidMode(copts.pidMode)
|
||||
if !pidMode.Valid() {
|
||||
return nil, errors.Errorf("--pid: invalid PID mode")
|
||||
@ -436,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
|
||||
}
|
||||
@ -549,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") {
|
||||
@ -579,7 +583,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
|
||||
ExtraHosts: copts.extraHosts.GetAll(),
|
||||
VolumesFrom: copts.volumesFrom.GetAll(),
|
||||
NetworkMode: container.NetworkMode(copts.netMode),
|
||||
IpcMode: container.IpcMode(copts.ipcMode),
|
||||
IpcMode: ipcMode,
|
||||
PidMode: pidMode,
|
||||
UTSMode: utsMode,
|
||||
UsernsMode: usernsMode,
|
||||
@ -662,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)
|
||||
}
|
||||
@ -824,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)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
@ -9,14 +11,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/internal/test/testutil"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
networktypes "github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/pkg/testutil"
|
||||
"github.com/docker/docker/runconfig"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateAttach(t *testing.T) {
|
||||
@ -43,9 +45,11 @@ func TestValidateAttach(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: unparam
|
||||
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
|
||||
flags, copts := setupRunFlags()
|
||||
flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
|
||||
flags.SetOutput(ioutil.Discard)
|
||||
flags.Usage = nil
|
||||
copts := addFlags(flags)
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@ -57,22 +61,16 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network
|
||||
return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err
|
||||
}
|
||||
|
||||
func setupRunFlags() (*pflag.FlagSet, *containerOptions) {
|
||||
flags := pflag.NewFlagSet("run", pflag.ContinueOnError)
|
||||
flags.SetOutput(ioutil.Discard)
|
||||
flags.Usage = nil
|
||||
copts := addFlags(flags)
|
||||
return flags, copts
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -88,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)
|
||||
@ -104,21 +103,35 @@ 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
|
||||
func TestParseWithVolumes(t *testing.T) {
|
||||
func TestParseRunVolumes(t *testing.T) {
|
||||
|
||||
// A single volume
|
||||
arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`})
|
||||
@ -138,19 +151,19 @@ func TestParseWithVolumes(t *testing.T) {
|
||||
t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes)
|
||||
}
|
||||
|
||||
// A single bind mount
|
||||
// A single bind-mount
|
||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`})
|
||||
if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] {
|
||||
t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes)
|
||||
}
|
||||
|
||||
// Two bind mounts.
|
||||
// Two bind-mounts.
|
||||
arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`})
|
||||
if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil {
|
||||
t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds)
|
||||
}
|
||||
|
||||
// Two bind mounts, first read-only, second read-write.
|
||||
// Two bind-mounts, first read-only, second read-write.
|
||||
// TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4
|
||||
arr, tryit = setupPlatformVolume(
|
||||
[]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`},
|
||||
@ -232,21 +245,20 @@ func TestParseWithMacAddress(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFlagsParseWithMemory(t *testing.T) {
|
||||
flags, _ := setupRunFlags()
|
||||
args := []string{"--memory=invalid", "img", "cmd"}
|
||||
err := flags.Parse(args)
|
||||
testutil.ErrorContains(t, err, `invalid argument "invalid" for "-m, --memory" flag`)
|
||||
func TestParseWithMemory(t *testing.T) {
|
||||
invalidMemory := "--memory=invalid"
|
||||
_, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, invalidMemory)
|
||||
|
||||
_, hostconfig := mustParse(t, "--memory=1G")
|
||||
assert.Equal(t, int64(1073741824), hostconfig.Memory)
|
||||
}
|
||||
|
||||
func TestParseWithMemorySwap(t *testing.T) {
|
||||
flags, _ := setupRunFlags()
|
||||
args := []string{"--memory-swap=invalid", "img", "cmd"}
|
||||
err := flags.Parse(args)
|
||||
testutil.ErrorContains(t, err, `invalid argument "invalid" for "--memory-swap" flag`)
|
||||
invalidMemory := "--memory-swap=invalid"
|
||||
|
||||
_, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"})
|
||||
testutil.ErrorContains(t, err, invalidMemory)
|
||||
|
||||
_, hostconfig := mustParse(t, "--memory-swap=1G")
|
||||
assert.Equal(t, int64(1073741824), hostconfig.MemorySwap)
|
||||
@ -370,43 +382,52 @@ func TestParseDevice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseModes(t *testing.T) {
|
||||
// ipc ko
|
||||
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"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hostconfig.IpcMode.Valid() {
|
||||
t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode)
|
||||
}
|
||||
// pid ko
|
||||
flags, copts := setupRunFlags()
|
||||
args := []string{"--pid=container:", "img", "cmd"}
|
||||
require.NoError(t, flags.Parse(args))
|
||||
_, err := parse(flags, copts)
|
||||
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)
|
||||
_, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFlagsParseShmSize(t *testing.T) {
|
||||
// shm-size ko
|
||||
flags, _ := setupRunFlags()
|
||||
args := []string{"--shm-size=a128m", "img", "cmd"}
|
||||
expectedErr := `invalid argument "a128m" for "--shm-size" flag: invalid size: 'a128m'`
|
||||
err := flags.Parse(args)
|
||||
testutil.ErrorContains(t, err, expectedErr)
|
||||
|
||||
expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'`
|
||||
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)
|
||||
_, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hostconfig.ShmSize != 134217728 {
|
||||
t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize)
|
||||
}
|
||||
@ -595,6 +616,213 @@ func TestParseEntryPoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// This tests the cases for binds which are generated through
|
||||
// DecodeContainerConfig rather than Parse()
|
||||
// nolint: gocyclo
|
||||
func TestDecodeContainerConfigVolumes(t *testing.T) {
|
||||
|
||||
// Root to root
|
||||
bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`})
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// No destination path
|
||||
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`})
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// // No destination path or mode
|
||||
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`})
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// A whole lot of nothing
|
||||
bindsOrVols = []string{`:`}
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// A whole lot of nothing with no mode
|
||||
bindsOrVols = []string{`::`}
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// Too much including an invalid mode
|
||||
wTmp := os.Getenv("TEMP")
|
||||
bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp})
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// Windows specific error tests
|
||||
if runtime.GOOS == "windows" {
|
||||
// Volume which does not include a drive letter
|
||||
bindsOrVols = []string{`\tmp`}
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// Root to C-Drive
|
||||
bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`}
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// Container path that does not include a drive letter
|
||||
bindsOrVols = []string{`c:\windows:\somewhere`}
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
}
|
||||
|
||||
// Linux-specific error tests
|
||||
if runtime.GOOS != "windows" {
|
||||
// Just root
|
||||
bindsOrVols = []string{`/`}
|
||||
if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil {
|
||||
t.Fatalf("binds %v should have failed", bindsOrVols)
|
||||
}
|
||||
if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil {
|
||||
t.Fatalf("volume %v should have failed", bindsOrVols)
|
||||
}
|
||||
|
||||
// A single volume that looks like a bind mount passed in Volumes.
|
||||
// This should be handled as a bind mount, not a volume.
|
||||
vols := []string{`/foo:/bar`}
|
||||
if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil {
|
||||
t.Fatal("Volume /foo:/bar should have succeeded as a volume name")
|
||||
} else if hostConfig.Binds != nil {
|
||||
t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds)
|
||||
} else if _, exists := config.Volumes[vols[0]]; !exists {
|
||||
t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes
|
||||
// to call DecodeContainerConfig. It effectively does what a client would
|
||||
// do when calling the daemon by constructing a JSON stream of a
|
||||
// ContainerConfigWrapper which is populated by the set of volume specs
|
||||
// passed into it. It returns a config and a hostconfig which can be
|
||||
// validated to ensure DecodeContainerConfig has manipulated the structures
|
||||
// correctly.
|
||||
func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) {
|
||||
var (
|
||||
b []byte
|
||||
err error
|
||||
c *container.Config
|
||||
h *container.HostConfig
|
||||
)
|
||||
w := runconfig.ContainerConfigWrapper{
|
||||
Config: &container.Config{
|
||||
Volumes: map[string]struct{}{},
|
||||
},
|
||||
HostConfig: &container.HostConfig{
|
||||
NetworkMode: "none",
|
||||
Binds: binds,
|
||||
},
|
||||
}
|
||||
for _, v := range volumes {
|
||||
w.Config.Volumes[v] = struct{}{}
|
||||
}
|
||||
if b, err = json.Marshal(w); err != nil {
|
||||
return nil, nil, errors.Errorf("Error on marshal %s", err.Error())
|
||||
}
|
||||
c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err)
|
||||
}
|
||||
if c == nil || h == nil {
|
||||
return nil, nil, errors.Errorf("Empty config or hostconfig")
|
||||
}
|
||||
|
||||
return c, h, err
|
||||
}
|
||||
|
||||
func 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",
|
||||
|
||||
@ -16,7 +16,7 @@ type pauseOptions struct {
|
||||
}
|
||||
|
||||
// NewPauseCommand creates a new cobra.Command for `docker pause`
|
||||
func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewPauseCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts pauseOptions
|
||||
|
||||
return &cobra.Command{
|
||||
@ -30,7 +30,7 @@ func NewPauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func runPause(dockerCli command.Cli, opts *pauseOptions) error {
|
||||
func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
|
||||
@ -19,7 +19,7 @@ type portOptions struct {
|
||||
}
|
||||
|
||||
// NewPortCommand creates a new cobra.Command for `docker port`
|
||||
func NewPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewPortCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts portOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -37,7 +37,7 @@ func NewPortCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runPort(dockerCli command.Cli, opts *portOptions) error {
|
||||
func runPort(dockerCli *command.DockerCli, opts *portOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
c, err := dockerCli.Client().ContainerInspect(ctx, opts.container)
|
||||
|
||||
@ -35,7 +35,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.25"},
|
||||
Tags: map[string]string{"version": "1.25"},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@ -52,12 +52,12 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
|
||||
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
|
||||
return 0, "", nil
|
||||
return
|
||||
}
|
||||
|
||||
report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters)
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
return
|
||||
}
|
||||
|
||||
if len(report.ContainersDeleted) > 0 {
|
||||
@ -68,7 +68,7 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
|
||||
spaceReclaimed = report.SpaceReclaimed
|
||||
}
|
||||
|
||||
return spaceReclaimed, output, nil
|
||||
return
|
||||
}
|
||||
|
||||
// RunPrune calls the Container Prune API
|
||||
|
||||
@ -17,7 +17,7 @@ type renameOptions struct {
|
||||
}
|
||||
|
||||
// NewRenameCommand creates a new cobra.Command for `docker rename`
|
||||
func NewRenameCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewRenameCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts renameOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -33,7 +33,7 @@ func NewRenameCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRename(dockerCli command.Cli, opts *renameOptions) error {
|
||||
func runRename(dockerCli *command.DockerCli, opts *renameOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
oldName := strings.TrimSpace(opts.oldName)
|
||||
|
||||
@ -20,7 +20,7 @@ type restartOptions struct {
|
||||
}
|
||||
|
||||
// NewRestartCommand creates a new cobra.Command for `docker restart`
|
||||
func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts restartOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -39,7 +39,7 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRestart(dockerCli command.Cli, opts *restartOptions) error {
|
||||
func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error {
|
||||
ctx := context.Background()
|
||||
var errs []string
|
||||
var timeout *time.Duration
|
||||
|
||||
@ -21,7 +21,7 @@ type rmOptions struct {
|
||||
}
|
||||
|
||||
// NewRmCommand creates a new cobra.Command for `docker rm`
|
||||
func NewRmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewRmCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts rmOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -41,7 +41,7 @@ func NewRmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRm(dockerCli command.Cli, opts *rmOptions) error {
|
||||
func runRm(dockerCli *command.DockerCli, opts *rmOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
|
||||
@ -5,20 +5,20 @@ import (
|
||||
"io"
|
||||
"net/http/httputil"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"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/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"golang.org/x/net/context"
|
||||
@ -29,11 +29,10 @@ type runOptions struct {
|
||||
sigProxy bool
|
||||
name string
|
||||
detachKeys string
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewRunCommand create a new `docker run` command
|
||||
func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts runOptions
|
||||
var copts *containerOptions
|
||||
|
||||
@ -63,7 +62,6 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
command.AddPlatformFlag(flags, &opts.platform)
|
||||
command.AddTrustVerificationFlags(flags)
|
||||
copts = addFlags(flags)
|
||||
return cmd
|
||||
@ -79,47 +77,25 @@ 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.Cli, 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
|
||||
func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error {
|
||||
func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error {
|
||||
config := containerConfig.Config
|
||||
hostConfig := containerConfig.HostConfig
|
||||
stdout, stderr := dockerCli.Out(), dockerCli.Err()
|
||||
@ -162,7 +138,7 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
|
||||
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
|
||||
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name, opts.platform)
|
||||
createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name)
|
||||
if err != nil {
|
||||
reportError(stderr, cmdPath, err.Error(), true)
|
||||
return runStartContainerErr(err)
|
||||
@ -171,7 +147,6 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
|
||||
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
var (
|
||||
waitDisplayID chan struct{}
|
||||
errCh chan error
|
||||
@ -191,11 +166,10 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
|
||||
}
|
||||
|
||||
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)
|
||||
@ -292,27 +266,22 @@ func attachContainer(
|
||||
return nil, errAttach
|
||||
}
|
||||
|
||||
ch := make(chan error, 1)
|
||||
*errCh = ch
|
||||
*errCh = promise.Go(func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: cerr,
|
||||
resp: resp,
|
||||
tty: config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
go func() {
|
||||
ch <- func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: cerr,
|
||||
resp: resp,
|
||||
tty: config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
if errHijack := streamer.stream(ctx); errHijack != nil {
|
||||
return errHijack
|
||||
}
|
||||
return errAttach
|
||||
}()
|
||||
}()
|
||||
if errHijack := streamer.stream(ctx); errHijack != nil {
|
||||
return errHijack
|
||||
}
|
||||
return errAttach
|
||||
})
|
||||
return resp.Close, nil
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/promise"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/pkg/errors"
|
||||
@ -27,7 +28,7 @@ type startOptions struct {
|
||||
}
|
||||
|
||||
// NewStartCommand creates a new cobra.Command for `docker start`
|
||||
func NewStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts startOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -53,7 +54,7 @@ func NewStartCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func runStart(dockerCli command.Cli, opts *startOptions) error {
|
||||
func runStart(dockerCli *command.DockerCli, opts *startOptions) error {
|
||||
ctx, cancelFun := context.WithCancel(context.Background())
|
||||
|
||||
if opts.attach || opts.openStdin {
|
||||
@ -102,28 +103,23 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
|
||||
return errAttach
|
||||
}
|
||||
defer resp.Close()
|
||||
cErr := promise.Go(func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: dockerCli.Out(),
|
||||
errorStream: dockerCli.Err(),
|
||||
resp: resp,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
cErr := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
cErr <- func() error {
|
||||
streamer := hijackedIOStreamer{
|
||||
streams: dockerCli,
|
||||
inputStream: in,
|
||||
outputStream: dockerCli.Out(),
|
||||
errorStream: dockerCli.Err(),
|
||||
resp: resp,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
|
||||
errHijack := streamer.stream(ctx)
|
||||
if errHijack == nil {
|
||||
return errAttach
|
||||
}
|
||||
return errHijack
|
||||
}()
|
||||
}()
|
||||
errHijack := streamer.stream(ctx)
|
||||
if errHijack == nil {
|
||||
return errAttach
|
||||
}
|
||||
return errHijack
|
||||
})
|
||||
|
||||
// 3. We should open a channel for receiving status code of the container
|
||||
// no matter it's detached, removed on daemon side(--rm) or exit normally.
|
||||
@ -150,12 +146,12 @@ func runStart(dockerCli command.Cli, 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 {
|
||||
@ -181,7 +177,7 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func startContainersWithoutAttachments(ctx context.Context, dockerCli command.Cli, containers []string) error {
|
||||
func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.DockerCli, containers []string) error {
|
||||
var failedContainers []string
|
||||
for _, container := range containers {
|
||||
if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil {
|
||||
|
||||
@ -21,13 +21,12 @@ import (
|
||||
type statsOptions struct {
|
||||
all bool
|
||||
noStream bool
|
||||
noTrunc bool
|
||||
format string
|
||||
containers []string
|
||||
}
|
||||
|
||||
// NewStatsCommand creates a new cobra.Command for `docker stats`
|
||||
func NewStatsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts statsOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -43,7 +42,6 @@ func NewStatsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)")
|
||||
flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result")
|
||||
flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output")
|
||||
flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template")
|
||||
return cmd
|
||||
}
|
||||
@ -51,7 +49,7 @@ func NewStatsCommand(dockerCli command.Cli) *cobra.Command {
|
||||
// runStats displays a live stream of resource usage statistics for one or more containers.
|
||||
// This shows real-time information on CPU usage, memory usage, and network I/O.
|
||||
// nolint: gocyclo
|
||||
func runStats(dockerCli command.Cli, opts *statsOptions) error {
|
||||
func runStats(dockerCli *command.DockerCli, opts *statsOptions) error {
|
||||
showAll := len(opts.containers) == 0
|
||||
closeChan := make(chan error)
|
||||
|
||||
@ -108,7 +106,7 @@ func runStats(dockerCli command.Cli, 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)
|
||||
@ -125,7 +123,7 @@ func runStats(dockerCli command.Cli, 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)
|
||||
@ -134,7 +132,7 @@ func runStats(dockerCli command.Cli, 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)
|
||||
@ -160,7 +158,7 @@ func runStats(dockerCli command.Cli, 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)
|
||||
@ -216,7 +214,7 @@ func runStats(dockerCli command.Cli, opts *statsOptions) error {
|
||||
ccstats = append(ccstats, c.GetStatistics())
|
||||
}
|
||||
cStats.mu.Unlock()
|
||||
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType, !opts.noTrunc); err != nil {
|
||||
if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil {
|
||||
break
|
||||
}
|
||||
if len(cStats.cs) == 0 && !showAll {
|
||||
|
||||
@ -7,17 +7,18 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
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
|
||||
@ -200,8 +201,7 @@ func calculateCPUPercentWindows(v *types.StatsJSON) float64 {
|
||||
return 0.00
|
||||
}
|
||||
|
||||
func calculateBlockIO(blkio types.BlkioStats) (uint64, uint64) {
|
||||
var blkRead, blkWrite uint64
|
||||
func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) {
|
||||
for _, bioEntry := range blkio.IoServiceBytesRecursive {
|
||||
switch strings.ToLower(bioEntry.Op) {
|
||||
case "read":
|
||||
@ -210,7 +210,7 @@ func calculateBlockIO(blkio types.BlkioStats) (uint64, uint64) {
|
||||
blkWrite = blkWrite + bioEntry.Value
|
||||
}
|
||||
}
|
||||
return blkRead, blkWrite
|
||||
return
|
||||
}
|
||||
|
||||
func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) {
|
||||
|
||||
@ -20,7 +20,7 @@ type stopOptions struct {
|
||||
}
|
||||
|
||||
// NewStopCommand creates a new cobra.Command for `docker stop`
|
||||
func NewStopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts stopOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -39,7 +39,7 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runStop(dockerCli command.Cli, opts *stopOptions) error {
|
||||
func runStop(dockerCli *command.DockerCli, opts *stopOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var timeout *time.Duration
|
||||
|
||||
@ -18,7 +18,7 @@ type topOptions struct {
|
||||
}
|
||||
|
||||
// NewTopCommand creates a new cobra.Command for `docker top`
|
||||
func NewTopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewTopCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts topOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -38,7 +38,7 @@ func NewTopCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runTop(dockerCli command.Cli, opts *topOptions) error {
|
||||
func runTop(dockerCli *command.DockerCli, opts *topOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args)
|
||||
|
||||
@ -7,11 +7,11 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/signal"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -16,7 +16,7 @@ type unpauseOptions struct {
|
||||
}
|
||||
|
||||
// NewUnpauseCommand creates a new cobra.Command for `docker unpause`
|
||||
func NewUnpauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewUnpauseCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts unpauseOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -31,7 +31,7 @@ func NewUnpauseCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUnpause(dockerCli command.Cli, opts *unpauseOptions) error {
|
||||
func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
|
||||
@ -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"
|
||||
@ -35,7 +36,7 @@ type updateOptions struct {
|
||||
}
|
||||
|
||||
// NewUpdateCommand creates a new cobra.Command for `docker update`
|
||||
func NewUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var options updateOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -72,7 +73,7 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdate(dockerCli command.Cli, options *updateOptions) error {
|
||||
func runUpdate(dockerCli *command.DockerCli, options *updateOptions) error {
|
||||
var err error
|
||||
|
||||
if options.nFlag == 0 {
|
||||
@ -81,7 +82,7 @@ func runUpdate(dockerCli command.Cli, 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
|
||||
}
|
||||
|
||||
@ -3,17 +3,18 @@ package container
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/sirupsen/logrus"
|
||||
clientapi "github.com/docker/docker/client"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func waitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID string, waitRemove bool) <-chan int {
|
||||
func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) <-chan int {
|
||||
if len(containerID) == 0 {
|
||||
// containerID can never be empty
|
||||
panic("Internal Error: waitExitOrRemoved needs a containerID as parameter")
|
||||
@ -47,7 +48,7 @@ func waitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID s
|
||||
return statusC
|
||||
}
|
||||
|
||||
func legacyWaitExitOrRemoved(ctx context.Context, dockerCli command.Cli, containerID string, waitRemove bool) <-chan int {
|
||||
func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) <-chan int {
|
||||
var removeErr error
|
||||
statusChan := make(chan int)
|
||||
exitCode := 125
|
||||
@ -124,6 +125,20 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli command.Cli, contain
|
||||
return statusChan
|
||||
}
|
||||
|
||||
// getExitCode performs an inspect on the container. It returns
|
||||
// the running state and the exit code.
|
||||
func getExitCode(ctx context.Context, dockerCli *command.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.
|
||||
if !clientapi.IsErrConnectionFailed(err) {
|
||||
return false, -1, err
|
||||
}
|
||||
return false, -1, nil
|
||||
}
|
||||
return c.State.Running, c.State.ExitCode, nil
|
||||
}
|
||||
|
||||
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
|
||||
if len(containers) == 0 {
|
||||
return nil
|
||||
|
||||
@ -16,7 +16,7 @@ type waitOptions struct {
|
||||
}
|
||||
|
||||
// NewWaitCommand creates a new cobra.Command for `docker wait`
|
||||
func NewWaitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
func NewWaitCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||
var opts waitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@ -32,7 +32,7 @@ func NewWaitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runWait(dockerCli command.Cli, opts *waitOptions) error {
|
||||
func runWait(dockerCli *command.DockerCli, opts *waitOptions) error {
|
||||
ctx := context.Background()
|
||||
|
||||
var errs []string
|
||||
|
||||
@ -3,8 +3,8 @@ package command
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
eventtypes "github.com/docker/docker/api/types/events"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// EventHandler is abstract interface for user to customize
|
||||
|
||||
@ -2,15 +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/go-units"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -164,22 +165,22 @@ func (c *containerContext) Image() string {
|
||||
func (c *containerContext) Command() string {
|
||||
command := c.c.Command
|
||||
if c.trunc {
|
||||
command = Ellipsis(command, 20)
|
||||
command = stringutils.Ellipsis(command, 20)
|
||||
}
|
||||
return strconv.Quote(command)
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -226,7 +227,7 @@ func (c *containerContext) Mounts() string {
|
||||
name = m.Name
|
||||
}
|
||||
if c.trunc {
|
||||
name = Ellipsis(name, 15)
|
||||
name = stringutils.Ellipsis(name, 15)
|
||||
}
|
||||
mounts = append(mounts, name)
|
||||
}
|
||||
@ -256,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
|
||||
}
|
||||
|
||||
@ -10,9 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContainerPsContext(t *testing.T) {
|
||||
@ -66,7 +64,7 @@ func TestContainerPsContext(t *testing.T) {
|
||||
Source: "/a/path",
|
||||
},
|
||||
},
|
||||
}, true, "this-is-a-long…", ctx.Mounts},
|
||||
}, true, "this-is-a-lo...", ctx.Mounts},
|
||||
{types.Container{
|
||||
Mounts: []types.MountPoint{
|
||||
{
|
||||
@ -228,10 +226,13 @@ 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)},
|
||||
string(golden.Get(t, "container-context-write-special-headers.golden")),
|
||||
`CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS
|
||||
conta "ubuntu" 24 hours ago//.FOOBAR_BAZ
|
||||
conta "ubuntu" 24 hours ago//.FOOBAR_BAR
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
@ -356,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -375,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -408,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func compareMultipleValues(t *testing.T, value, expected string) {
|
||||
@ -23,5 +22,7 @@ func compareMultipleValues(t *testing.T, value, expected string) {
|
||||
keyval := strings.Split(expected, "=")
|
||||
expMap[keyval[0]] = keyval[1]
|
||||
}
|
||||
assert.Equal(t, expMap, entriesMap)
|
||||
if !reflect.DeepEqual(expMap, entriesMap) {
|
||||
t.Fatalf("Expected entries: %v, got: %v", expected, value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,63 +65,53 @@ 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() error {
|
||||
// First images
|
||||
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Output.Write([]byte("Images space usage:\n\n"))
|
||||
@ -141,14 +130,14 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
}
|
||||
}
|
||||
|
||||
err := ctx.contextFormat(tmpl, &imageContext{
|
||||
err = ctx.contextFormat(tmpl, &imageContext{
|
||||
repo: repo,
|
||||
tag: tag,
|
||||
trunc: true,
|
||||
i: *i,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newImageContext())
|
||||
@ -157,14 +146,17 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
for _, c := range ctx.Containers {
|
||||
// Don't display the virtual size
|
||||
c.SizeRootFs = 0
|
||||
err := ctx.contextFormat(tmpl, &containerContext{trunc: true, c: *c})
|
||||
err = ctx.contextFormat(tmpl, &containerContext{
|
||||
trunc: true,
|
||||
c: *c,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newContainerContext())
|
||||
@ -173,18 +165,18 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
for _, v := range ctx.Volumes {
|
||||
if err := ctx.contextFormat(tmpl, &volumeContext{v: *v}); err != nil {
|
||||
return err
|
||||
err = ctx.contextFormat(tmpl, &volumeContext{
|
||||
v: *v,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newVolumeContext())
|
||||
|
||||
// And build cache
|
||||
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
type diskUsageImagesContext struct {
|
||||
@ -237,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
|
||||
}
|
||||
|
||||
@ -299,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
|
||||
}
|
||||
|
||||
@ -360,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)))
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -24,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
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -40,9 +38,6 @@ CONTAINER ID IMAGE COMMAND LOCAL VOLUMES
|
||||
Local Volumes space usage:
|
||||
|
||||
VOLUME NAME LINKS SIZE
|
||||
|
||||
Build cache usage: 0B
|
||||
|
||||
`,
|
||||
},
|
||||
// Errors
|
||||
@ -75,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
|
||||
`,
|
||||
},
|
||||
{
|
||||
@ -84,7 +78,11 @@ Build Cache 0B
|
||||
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
|
||||
},
|
||||
},
|
||||
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
|
||||
`TYPE ACTIVE
|
||||
Images 0
|
||||
Containers 0
|
||||
Local Volumes 0
|
||||
`,
|
||||
},
|
||||
// Raw Format
|
||||
{
|
||||
@ -93,7 +91,25 @@ Build Cache 0B
|
||||
Format: NewDiskUsageFormat("raw"),
|
||||
},
|
||||
},
|
||||
string(golden.Get(t, "disk-usage-raw-format.golden")),
|
||||
`type: Images
|
||||
total: 0
|
||||
active: 0
|
||||
size: 0B
|
||||
reclaimable: 0B
|
||||
|
||||
type: Containers
|
||||
total: 0
|
||||
active: 0
|
||||
size: 0B
|
||||
reclaimable: 0B
|
||||
|
||||
type: Local Volumes
|
||||
total: 0
|
||||
active: 0
|
||||
size: 0B
|
||||
reclaimable: 0B
|
||||
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/width"
|
||||
)
|
||||
|
||||
// charWidth returns the number of horizontal positions a character occupies,
|
||||
// and is used to account for wide characters when displaying strings.
|
||||
//
|
||||
// In a broad sense, wide characters include East Asian Wide, East Asian Full-width,
|
||||
// (when not in East Asian context) see http://unicode.org/reports/tr11/.
|
||||
func charWidth(r rune) int {
|
||||
switch width.LookupRune(r).Kind() {
|
||||
case width.EastAsianWide, width.EastAsianFullwidth:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…).
|
||||
// For maxDisplayWidth of 1 and lower, no ellipsis is appended.
|
||||
// For maxDisplayWidth of 1, first char of string will return even if its width > 1.
|
||||
func Ellipsis(s string, maxDisplayWidth int) string {
|
||||
if maxDisplayWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
rs := []rune(s)
|
||||
if maxDisplayWidth == 1 {
|
||||
return string(rs[0])
|
||||
}
|
||||
|
||||
byteLen := len(s)
|
||||
if byteLen == utf8.RuneCountInString(s) {
|
||||
if byteLen <= maxDisplayWidth {
|
||||
return s
|
||||
}
|
||||
return string(rs[:maxDisplayWidth-1]) + "…"
|
||||
}
|
||||
|
||||
var (
|
||||
display []int
|
||||
displayWidth int
|
||||
)
|
||||
for _, r := range rs {
|
||||
cw := charWidth(r)
|
||||
displayWidth += cw
|
||||
display = append(display, displayWidth)
|
||||
}
|
||||
if displayWidth <= maxDisplayWidth {
|
||||
return s
|
||||
}
|
||||
for i := range display {
|
||||
if display[i] <= maxDisplayWidth-1 && display[i+1] > maxDisplayWidth-1 {
|
||||
return string(rs[:i+1]) + "…"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEllipsis(t *testing.T) {
|
||||
var testcases = []struct {
|
||||
source string
|
||||
width int
|
||||
expected string
|
||||
}{
|
||||
{source: "t🐳ststring", width: 0, expected: ""},
|
||||
{source: "t🐳ststring", width: 1, expected: "t"},
|
||||
{source: "t🐳ststring", width: 2, expected: "t…"},
|
||||
{source: "t🐳ststring", width: 6, expected: "t🐳st…"},
|
||||
{source: "t🐳ststring", width: 20, expected: "t🐳ststring"},
|
||||
{source: "你好世界teststring", width: 0, expected: ""},
|
||||
{source: "你好世界teststring", width: 1, expected: "你"},
|
||||
{source: "你好世界teststring", width: 3, expected: "你…"},
|
||||
{source: "你好世界teststring", width: 6, expected: "你好…"},
|
||||
{source: "你好世界teststring", width: 20, expected: "你好世界teststring"},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
assert.Equal(t, testcase.expected, Ellipsis(testcase.source, testcase.width))
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ import (
|
||||
"text/tabwriter"
|
||||
"text/template"
|
||||
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/pkg/templates"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
units "github.com/docker/go-units"
|
||||
)
|
||||
|
||||
@ -78,21 +79,21 @@ func (c *historyContext) ID() string {
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedAt() string {
|
||||
return time.Unix(c.h.Created, 0).Format(time.RFC3339)
|
||||
var created string
|
||||
created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0)))
|
||||
return created
|
||||
}
|
||||
|
||||
func (c *historyContext) CreatedSince() string {
|
||||
if !c.human {
|
||||
return c.CreatedAt()
|
||||
}
|
||||
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 Ellipsis(createdBy, 45)
|
||||
createdBy = stringutils.Ellipsis(createdBy, 45)
|
||||
}
|
||||
return createdBy
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@ -50,21 +51,17 @@ func TestHistoryContext_ID(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHistoryContext_CreatedSince(t *testing.T) {
|
||||
unixTime := time.Now().AddDate(0, 0, -7).Unix()
|
||||
expected := "7 days ago"
|
||||
|
||||
var ctx historyContext
|
||||
cases := []historyCase{
|
||||
{
|
||||
historyContext{
|
||||
h: image.HistoryResponseItem{Created: time.Now().AddDate(0, 0, -7).Unix()},
|
||||
h: image.HistoryResponseItem{Created: unixTime},
|
||||
trunc: false,
|
||||
human: true,
|
||||
}, "7 days ago", ctx.CreatedSince,
|
||||
},
|
||||
{
|
||||
historyContext{
|
||||
h: image.HistoryResponseItem{Created: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).Unix()},
|
||||
trunc: false,
|
||||
human: false,
|
||||
}, "2009-11-10T23:00:00Z", ctx.CreatedSince,
|
||||
}, expected, ctx.CreatedSince,
|
||||
},
|
||||
}
|
||||
|
||||
@ -95,7 +92,7 @@ func TestHistoryContext_CreatedBy(t *testing.T) {
|
||||
historyContext{
|
||||
h: image.HistoryResponseItem{CreatedBy: withTabs},
|
||||
trunc: true,
|
||||
}, Ellipsis(expected, 45), ctx.CreatedBy,
|
||||
}, stringutils.Ellipsis(expected, 45), ctx.CreatedBy,
|
||||
},
|
||||
}
|
||||
|
||||
@ -190,7 +187,7 @@ imageID3 24 hours ago /bin/bash ls
|
||||
imageID4 24 hours ago /bin/bash grep 183MB Hi
|
||||
`
|
||||
expectedTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT
|
||||
imageID1 24 hours ago /bin/bash ls && npm i && npm run test && kar… 183MB Hi
|
||||
imageID1 24 hours ago /bin/bash ls && npm i && npm run test && k... 183MB Hi
|
||||
imageID2 24 hours ago /bin/bash echo 183MB Hi
|
||||
imageID3 24 hours ago /bin/bash ls 183MB Hi
|
||||
imageID4 24 hours ago /bin/bash grep 183MB Hi
|
||||
|
||||
@ -79,16 +79,11 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error {
|
||||
return ctx.Write(newImageContext(), render)
|
||||
}
|
||||
|
||||
// needDigest determines whether the image digest should be ignored or not when writing image context
|
||||
func needDigest(ctx ImageContext) bool {
|
||||
return ctx.Digest || ctx.Format.Contains("{{.Digest}}")
|
||||
}
|
||||
|
||||
func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error {
|
||||
for _, image := range images {
|
||||
formatted := []*imageContext{}
|
||||
images := []*imageContext{}
|
||||
if isDangling(image) {
|
||||
formatted = append(formatted, &imageContext{
|
||||
images = append(images, &imageContext{
|
||||
trunc: ctx.Trunc,
|
||||
i: image,
|
||||
repo: "<none>",
|
||||
@ -96,9 +91,90 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
|
||||
digest: "<none>",
|
||||
})
|
||||
} else {
|
||||
formatted = imageFormatTaggedAndDigest(ctx, image)
|
||||
repoTags := map[string][]string{}
|
||||
repoDigests := map[string][]string{}
|
||||
|
||||
for _, refString := range image.RepoTags {
|
||||
ref, err := reference.ParseNormalizedNamed(refString)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if nt, ok := ref.(reference.NamedTagged); ok {
|
||||
familiarRef := reference.FamiliarName(ref)
|
||||
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
|
||||
}
|
||||
}
|
||||
for _, refString := range image.RepoDigests {
|
||||
ref, err := reference.ParseNormalizedNamed(refString)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if c, ok := ref.(reference.Canonical); ok {
|
||||
familiarRef := reference.FamiliarName(ref)
|
||||
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
|
||||
}
|
||||
}
|
||||
|
||||
for repo, tags := range repoTags {
|
||||
digests := repoDigests[repo]
|
||||
|
||||
// Do not display digests as their own row
|
||||
delete(repoDigests, repo)
|
||||
|
||||
if !ctx.Digest {
|
||||
// Ignore digest references, just show tag once
|
||||
digests = nil
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if len(digests) == 0 {
|
||||
images = append(images, &imageContext{
|
||||
trunc: ctx.Trunc,
|
||||
i: image,
|
||||
repo: repo,
|
||||
tag: tag,
|
||||
digest: "<none>",
|
||||
})
|
||||
continue
|
||||
}
|
||||
// Display the digests for each tag
|
||||
for _, dgst := range digests {
|
||||
images = append(images, &imageContext{
|
||||
trunc: ctx.Trunc,
|
||||
i: image,
|
||||
repo: repo,
|
||||
tag: tag,
|
||||
digest: dgst,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Show rows for remaining digest only references
|
||||
for repo, digests := range repoDigests {
|
||||
// If digests are displayed, show row per digest
|
||||
if ctx.Digest {
|
||||
for _, dgst := range digests {
|
||||
images = append(images, &imageContext{
|
||||
trunc: ctx.Trunc,
|
||||
i: image,
|
||||
repo: repo,
|
||||
tag: "<none>",
|
||||
digest: dgst,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
images = append(images, &imageContext{
|
||||
trunc: ctx.Trunc,
|
||||
i: image,
|
||||
repo: repo,
|
||||
tag: "<none>",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, imageCtx := range formatted {
|
||||
for _, imageCtx := range images {
|
||||
if err := format(imageCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -107,82 +183,6 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageFormatTaggedAndDigest(ctx ImageContext, image types.ImageSummary) []*imageContext {
|
||||
repoTags := map[string][]string{}
|
||||
repoDigests := map[string][]string{}
|
||||
images := []*imageContext{}
|
||||
|
||||
for _, refString := range image.RepoTags {
|
||||
ref, err := reference.ParseNormalizedNamed(refString)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if nt, ok := ref.(reference.NamedTagged); ok {
|
||||
familiarRef := reference.FamiliarName(ref)
|
||||
repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag())
|
||||
}
|
||||
}
|
||||
for _, refString := range image.RepoDigests {
|
||||
ref, err := reference.ParseNormalizedNamed(refString)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if c, ok := ref.(reference.Canonical); ok {
|
||||
familiarRef := reference.FamiliarName(ref)
|
||||
repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String())
|
||||
}
|
||||
}
|
||||
|
||||
addImage := func(repo, tag, digest string) {
|
||||
image := &imageContext{
|
||||
trunc: ctx.Trunc,
|
||||
i: image,
|
||||
repo: repo,
|
||||
tag: tag,
|
||||
digest: digest,
|
||||
}
|
||||
images = append(images, image)
|
||||
}
|
||||
|
||||
for repo, tags := range repoTags {
|
||||
digests := repoDigests[repo]
|
||||
|
||||
// Do not display digests as their own row
|
||||
delete(repoDigests, repo)
|
||||
|
||||
if !needDigest(ctx) {
|
||||
// Ignore digest references, just show tag once
|
||||
digests = nil
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
if len(digests) == 0 {
|
||||
addImage(repo, tag, "<none>")
|
||||
continue
|
||||
}
|
||||
// Display the digests for each tag
|
||||
for _, dgst := range digests {
|
||||
addImage(repo, tag, dgst)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Show rows for remaining digest only references
|
||||
for repo, digests := range repoDigests {
|
||||
// If digests are displayed, show row per digest
|
||||
if ctx.Digest {
|
||||
for _, dgst := range digests {
|
||||
addImage(repo, "<none>", dgst)
|
||||
}
|
||||
} else {
|
||||
addImage(repo, "<none>", "")
|
||||
|
||||
}
|
||||
}
|
||||
return images
|
||||
}
|
||||
|
||||
type imageContext struct {
|
||||
HeaderContext
|
||||
trunc bool
|
||||
@ -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 {
|
||||
|
||||
@ -55,26 +55,6 @@ func TestImageContext(t *testing.T) {
|
||||
i: types.ImageSummary{},
|
||||
digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
|
||||
}, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest},
|
||||
{
|
||||
imageContext{
|
||||
i: types.ImageSummary{Containers: 10},
|
||||
}, "10", ctx.Containers,
|
||||
},
|
||||
{
|
||||
imageContext{
|
||||
i: types.ImageSummary{VirtualSize: 10000},
|
||||
}, "10kB", ctx.VirtualSize,
|
||||
},
|
||||
{
|
||||
imageContext{
|
||||
i: types.ImageSummary{SharedSize: 10000},
|
||||
}, "10kB", ctx.SharedSize,
|
||||
},
|
||||
{
|
||||
imageContext{
|
||||
i: types.ImageSummary{SharedSize: 5000, VirtualSize: 20000},
|
||||
}, "15kB", ctx.UniqueSize,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
@ -82,8 +62,8 @@ func TestImageContext(t *testing.T) {
|
||||
v := c.call()
|
||||
if strings.Contains(v, ",") {
|
||||
compareMultipleValues(t, v, c.expValue)
|
||||
} else {
|
||||
assert.Equal(t, c.expValue, v)
|
||||
} else if v != c.expValue {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,14 +137,6 @@ image <none>
|
||||
},
|
||||
"REPOSITORY\nimage\nimage\n<none>\n",
|
||||
},
|
||||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
Format: NewImageFormat("table {{.Digest}}", true, false),
|
||||
},
|
||||
},
|
||||
"DIGEST\nsha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf\n<none>\n<none>\n",
|
||||
},
|
||||
{
|
||||
ImageContext{
|
||||
Context: Context{
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/stringid"
|
||||
"github.com/docker/docker/pkg/stringutils"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -79,7 +80,7 @@ func (c *pluginContext) Description() string {
|
||||
desc := strings.Replace(c.p.Config.Description, "\n", "", -1)
|
||||
desc = strings.Replace(desc, "\r", "", -1)
|
||||
if c.trunc {
|
||||
desc = Ellipsis(desc, 45)
|
||||
desc = stringutils.Ellipsis(desc, 45)
|
||||
}
|
||||
|
||||
return desc
|
||||
|
||||
@ -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)"
|
||||
}
|
||||
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
registry "github.com/docker/docker/api/types/registry"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSearchTableFormat = "table {{.Name}}\t{{.Description}}\t{{.StarCount}}\t{{.IsOfficial}}\t{{.IsAutomated}}"
|
||||
|
||||
starsHeader = "STARS"
|
||||
officialHeader = "OFFICIAL"
|
||||
automatedHeader = "AUTOMATED"
|
||||
)
|
||||
|
||||
// NewSearchFormat returns a Format for rendering using a network Context
|
||||
func NewSearchFormat(source string) Format {
|
||||
switch source {
|
||||
case "":
|
||||
return defaultSearchTableFormat
|
||||
case TableFormatKey:
|
||||
return defaultSearchTableFormat
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
// SearchWrite writes the context
|
||||
func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars int) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, result := range results {
|
||||
// --automated and -s, --stars are deprecated since Docker 1.12
|
||||
if (auto && !result.IsAutomated) || (stars > result.StarCount) {
|
||||
continue
|
||||
}
|
||||
searchCtx := &searchContext{trunc: ctx.Trunc, s: result}
|
||||
if err := format(searchCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
searchCtx := searchContext{}
|
||||
searchCtx.header = map[string]string{
|
||||
"Name": nameHeader,
|
||||
"Description": descriptionHeader,
|
||||
"StarCount": starsHeader,
|
||||
"IsOfficial": officialHeader,
|
||||
"IsAutomated": automatedHeader,
|
||||
}
|
||||
return ctx.Write(&searchCtx, render)
|
||||
}
|
||||
|
||||
type searchContext struct {
|
||||
HeaderContext
|
||||
trunc bool
|
||||
json bool
|
||||
s registry.SearchResult
|
||||
}
|
||||
|
||||
func (c *searchContext) MarshalJSON() ([]byte, error) {
|
||||
c.json = true
|
||||
return marshalJSON(c)
|
||||
}
|
||||
|
||||
func (c *searchContext) Name() string {
|
||||
return c.s.Name
|
||||
}
|
||||
|
||||
func (c *searchContext) Description() string {
|
||||
desc := strings.Replace(c.s.Description, "\n", " ", -1)
|
||||
desc = strings.Replace(desc, "\r", " ", -1)
|
||||
if c.trunc {
|
||||
desc = Ellipsis(desc, 45)
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
func (c *searchContext) StarCount() string {
|
||||
return strconv.Itoa(c.s.StarCount)
|
||||
}
|
||||
|
||||
func (c *searchContext) formatBool(value bool) string {
|
||||
switch {
|
||||
case value && c.json:
|
||||
return "true"
|
||||
case value:
|
||||
return "[OK]"
|
||||
case c.json:
|
||||
return "false"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c *searchContext) IsOfficial() string {
|
||||
return c.formatBool(c.s.IsOfficial)
|
||||
}
|
||||
|
||||
func (c *searchContext) IsAutomated() string {
|
||||
return c.formatBool(c.s.IsAutomated)
|
||||
}
|
||||
@ -1,279 +0,0 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/gotestyourself/gotestyourself/golden"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSearchContext(t *testing.T) {
|
||||
name := "nginx"
|
||||
starCount := 5000
|
||||
|
||||
var ctx searchContext
|
||||
cases := []struct {
|
||||
searchCtx searchContext
|
||||
expValue string
|
||||
call func() string
|
||||
}{
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Name: name},
|
||||
}, name, ctx.Name},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{StarCount: starCount},
|
||||
}, "5000", ctx.StarCount},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{IsOfficial: true},
|
||||
}, "[OK]", ctx.IsOfficial},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{IsOfficial: false},
|
||||
}, "", ctx.IsOfficial},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{IsAutomated: true},
|
||||
}, "[OK]", ctx.IsAutomated},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{IsAutomated: false},
|
||||
}, "", ctx.IsAutomated},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
ctx = c.searchCtx
|
||||
v := c.call()
|
||||
if strings.Contains(v, ",") {
|
||||
compareMultipleValues(t, v, c.expValue)
|
||||
} else if v != c.expValue {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextDescription(t *testing.T) {
|
||||
shortDescription := "Official build of Nginx."
|
||||
longDescription := "Automated Nginx reverse proxy for docker containers"
|
||||
descriptionWReturns := "Automated\nNginx reverse\rproxy\rfor docker\ncontainers"
|
||||
|
||||
var ctx searchContext
|
||||
cases := []struct {
|
||||
searchCtx searchContext
|
||||
expValue string
|
||||
call func() string
|
||||
}{
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Description: shortDescription},
|
||||
trunc: true,
|
||||
}, shortDescription, ctx.Description},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Description: shortDescription},
|
||||
trunc: false,
|
||||
}, shortDescription, ctx.Description},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Description: longDescription},
|
||||
trunc: false,
|
||||
}, longDescription, ctx.Description},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Description: longDescription},
|
||||
trunc: true,
|
||||
}, Ellipsis(longDescription, 45), ctx.Description},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Description: descriptionWReturns},
|
||||
trunc: false,
|
||||
}, longDescription, ctx.Description},
|
||||
{searchContext{
|
||||
s: registrytypes.SearchResult{Description: descriptionWReturns},
|
||||
trunc: true,
|
||||
}, Ellipsis(longDescription, 45), ctx.Description},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
ctx = c.searchCtx
|
||||
v := c.call()
|
||||
if strings.Contains(v, ",") {
|
||||
compareMultipleValues(t, v, c.expValue)
|
||||
} else if v != c.expValue {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextWrite(t *testing.T) {
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
|
||||
// Errors
|
||||
{
|
||||
Context{Format: "{{InvalidFunction}}"},
|
||||
`Template parsing error: template: :1: function "InvalidFunction" not defined
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: "{{nil}}"},
|
||||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
|
||||
`,
|
||||
},
|
||||
// Table format
|
||||
{
|
||||
Context{Format: NewSearchFormat("table")},
|
||||
string(golden.Get(t, "search-context-write-table.golden")),
|
||||
},
|
||||
{
|
||||
Context{Format: NewSearchFormat("table {{.Name}}")},
|
||||
`NAME
|
||||
result1
|
||||
result2
|
||||
`,
|
||||
},
|
||||
// Custom Format
|
||||
{
|
||||
Context{Format: NewSearchFormat("{{.Name}}")},
|
||||
`result1
|
||||
result2
|
||||
`,
|
||||
},
|
||||
// Custom Format with CreatedAt
|
||||
{
|
||||
Context{Format: NewSearchFormat("{{.Name}} {{.StarCount}}")},
|
||||
`result1 5000
|
||||
result2 5
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
results := []registrytypes.SearchResult{
|
||||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
|
||||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := SearchWrite(testcase.context, results, false, 0)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextWriteAutomated(t *testing.T) {
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
|
||||
// Table format
|
||||
{
|
||||
Context{Format: NewSearchFormat("table")},
|
||||
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
|
||||
result2 Not official 5 [OK]
|
||||
`,
|
||||
},
|
||||
{
|
||||
Context{Format: NewSearchFormat("table {{.Name}}")},
|
||||
`NAME
|
||||
result2
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
results := []registrytypes.SearchResult{
|
||||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
|
||||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := SearchWrite(testcase.context, results, true, 0)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextWriteStars(t *testing.T) {
|
||||
cases := []struct {
|
||||
context Context
|
||||
expected string
|
||||
}{
|
||||
|
||||
// Table format
|
||||
{
|
||||
Context{Format: NewSearchFormat("table")},
|
||||
string(golden.Get(t, "search-context-write-stars-table.golden")),
|
||||
},
|
||||
{
|
||||
Context{Format: NewSearchFormat("table {{.Name}}")},
|
||||
`NAME
|
||||
result1
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
results := []registrytypes.SearchResult{
|
||||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
|
||||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
testcase.context.Output = out
|
||||
err := SearchWrite(testcase.context, results, false, 6)
|
||||
if err != nil {
|
||||
assert.Error(t, err, testcase.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), testcase.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextWriteJSON(t *testing.T) {
|
||||
results := []registrytypes.SearchResult{
|
||||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
|
||||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
|
||||
}
|
||||
expectedJSONs := []map[string]interface{}{
|
||||
{"Name": "result1", "Description": "Official build", "StarCount": "5000", "IsOfficial": "true", "IsAutomated": "false"},
|
||||
{"Name": "result2", "Description": "Not official", "StarCount": "5", "IsOfficial": "false", "IsAutomated": "true"},
|
||||
}
|
||||
|
||||
out := bytes.NewBufferString("")
|
||||
err := SearchWrite(Context{Format: "{{json .}}", Output: out}, results, false, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(line), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, m, expectedJSONs[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchContextWriteJSONField(t *testing.T) {
|
||||
results := []registrytypes.SearchResult{
|
||||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false},
|
||||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true},
|
||||
}
|
||||
out := bytes.NewBufferString("")
|
||||
err := SearchWrite(Context{Format: "{{json .Name}}", Output: out}, results, false, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
||||
t.Logf("Output: line %d: %s", i, line)
|
||||
var s string
|
||||
if err := json.Unmarshal([]byte(line), &s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, s, results[i].Name)
|
||||
}
|
||||
}
|
||||
@ -12,20 +12,19 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
|
||||
defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}"
|
||||
secretIDHeader = "ID"
|
||||
secretCreatedHeader = "CREATED"
|
||||
secretUpdatedHeader = "UPDATED"
|
||||
secretInspectPrettyTemplate Format = `ID: {{.ID}}
|
||||
Name: {{.Name}}
|
||||
secretInspectPrettyTemplate Format = `ID: {{.ID}}
|
||||
Name: {{.Name}}
|
||||
{{- if .Labels }}
|
||||
Labels:
|
||||
{{- range $k, $v := .Labels }}
|
||||
- {{ $k }}{{if $v }}={{ $v }}{{ end }}
|
||||
{{- end }}{{ end }}
|
||||
Driver: {{.Driver}}
|
||||
Created at: {{.CreatedAt}}
|
||||
Updated at: {{.UpdatedAt}}`
|
||||
Created at: {{.CreatedAt}}
|
||||
Updated at: {{.UpdatedAt}}`
|
||||
)
|
||||
|
||||
// NewSecretFormat returns a Format for rendering using a secret Context
|
||||
@ -62,7 +61,6 @@ func newSecretContext() *secretContext {
|
||||
sCtx.header = map[string]string{
|
||||
"ID": secretIDHeader,
|
||||
"Name": nameHeader,
|
||||
"Driver": driverHeader,
|
||||
"CreatedAt": secretCreatedHeader,
|
||||
"UpdatedAt": secretUpdatedHeader,
|
||||
"Labels": labelsHeader,
|
||||
@ -91,13 +89,6 @@ func (c *secretContext) CreatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *secretContext) Driver() string {
|
||||
if c.s.Spec.Driver == nil {
|
||||
return ""
|
||||
}
|
||||
return c.s.Spec.Driver.Name
|
||||
}
|
||||
|
||||
func (c *secretContext) UpdatedAt() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago"
|
||||
}
|
||||
@ -162,13 +153,6 @@ func (ctx *secretInspectContext) Labels() map[string]string {
|
||||
return ctx.Secret.Spec.Labels
|
||||
}
|
||||
|
||||
func (ctx *secretInspectContext) Driver() string {
|
||||
if ctx.Secret.Spec.Driver == nil {
|
||||
return ""
|
||||
}
|
||||
return ctx.Secret.Spec.Driver.Name
|
||||
}
|
||||
|
||||
func (ctx *secretInspectContext) CreatedAt() string {
|
||||
return command.PrettyPrint(ctx.Secret.CreatedAt)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user