Compare commits

..

28 Commits

Author SHA1 Message Date
8c4be39ddd Merge pull request #141 from andrewhsu/ver
bump version to 17.07.0-ce-rc1
2017-07-25 18:38:06 -07:00
450a1a63d7 bump version to 17.07.0-ce-rc1
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2017-07-25 18:35:19 -07:00
b3be2b02db Merge pull request #124 from vieux/17.07-changelog
[17.07.x] Changelog
2017-07-25 18:33:07 -07:00
3ddc583fc2 Merge pull request #139 from cyli/re-vendor-swarmkit-17.07
[17.07] Re-vendor swarmkit
2017-07-25 18:31:57 -07:00
b92b043ac1 Re-vendors swarmkit to include the following fix:
- https://github.com/docker/swarmkit/pull/2323 (fix for watch server being run only on leader)

Signed-off-by: Ying <ying.li@docker.com>
2017-07-25 16:16:43 -07:00
1c65e20e5b Merge pull request #136 from cyli/re-vendor-swarmkit-17.07
[17.07] Re-vendor swarmkit
2017-07-25 15:23:43 -07:00
1fef2502d2 Merge pull request #131 from vieux/backport-fix-live-restore
[17.07] Graceful upgrade of containerd and runc state files upon live-restore
2017-07-25 15:19:55 -07:00
edadfd04be Merge pull request #137 from seemethere/cherry_pick_packaging_3_11_40_41
[17.07] Cherry pick latest packaging commits
2017-07-25 14:02:47 -07:00
83e8a29ede Removes telemetry mentions from Fedora 26
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit a8c7e75bfd263b2f7445b06f749aba13aaeac789)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 11:29:40 -07:00
0ff00e73c9 End Ubuntu 16.10 (Yakkety) support
Ubuntu 16.10 (Yakkety) reached it's "End of Life" on July 20, 2017:
http://fridge.ubuntu.com/2017/07/20/ubuntu-16-10-yakkety-yak-end-of-life-reached-on-july-20-2017/

As such we are removing support for the distribution for future versions
of Docker packages.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 36cc14cd480879391eeb9f470417698618f4cdb3)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:31:41 -07:00
32709fc76b Update spec to align with new spec files
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit feb2f647c5570753a607b71bb476028c8d1b4e55)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:30:18 -07:00
95fa7a2d20 Update go version, add distro/suite tags
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 26b1bd9f3da46b907021ae66d310405057e29634)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:30:18 -07:00
22f30a8bb1 build fedora 26 packages
Just released: https://docs.fedoraproject.org/en-US/Fedora/26/html/Release_Notes/

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
(cherry picked from commit 412824b465d8334278bb9c02f0a5ee3d294826fd)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:30:16 -07:00
e5f33c5d16 Revert "Added the metrics plugin to the RPM/DEB packages"
This reverts commit ceac22bf2240df483b1d3bd3a19ac681e665f910.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit bda73f99e455bd3f041ccc843f094186ca8b8623)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:20:14 -07:00
c203be7748 Tell build-deb to throw up errors
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 3618e4aa262f154f79c804752d69110772e636a3)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:19:29 -07:00
b9b61ead5a Add raspbian deb packages
(cherry picked from commit 1c61fdcafc4f6fa0e41c267cd857d0b60d3a2dbd)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:19:20 -07:00
8ddc1eeedd Added the metrics plugin to the RPM/DEB packages
- centos
 - fedora
 - ubuntu
 - debian

Signed-off-by: Roberto Gandolfo Hashioka <roberto_hashioka@hotmail.com>
(cherry picked from commit ceac22bf2240df483b1d3bd3a19ac681e665f910)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-25 10:18:39 -07:00
1dd11b3bb6 Re-vendors swarmkit to include the following fixes:
- https://github.com/docker/swarmkit/pull/2288 (Allow updates of failed services with restart policy "none")
- https://github.com/docker/swarmkit/pull/2304 (Reset restart history when task spec changes)
- https://github.com/docker/swarmkit/pull/2309 (updating the service spec version when rolling back)
- https://github.com/docker/swarmkit/pull/2310 (fix for slow swarm shutdown)

Signed-off-by: Ying <ying.li@docker.com>
2017-07-24 18:01:58 -07:00
9e07b412c7 add 17.07 changelog
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-07-24 17:10:41 -07:00
1b065d3124 [engine] Graceful upgrade of containerd and runc state files upon live-restore
Vendors new dependency github.com/crosbymichael/upgrade

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 358c36e930)
Signed-off-by: Victor Vieux <victorvieux@gmail.com>
2017-07-24 16:51:35 -07:00
bd4b12ce5d Merge pull request #128 from seemethere/cherry_pick_packaging_37
[17.07.x] Change make -C to a cd command
2017-07-24 14:57:23 -07:00
c249bb0aea Change make -C to a cd command
Tried out make -C in this scenario and it did not seem to function
correctly, changed to cd.

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 3a548f8815d5308b197abea1e39f0a0a4939c4f2)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-24 14:36:41 -07:00
cb16f8905b Merge pull request #126 from seemethere/cherry_pick_moby_34206
[17.07.x] Add go-autogen to integration tests
2017-07-21 15:26:19 -07:00
c4d547ce61 Add go-autogen to integration tests
Integration test were failing in trial runs for docker-ce 17.07 due to
the lack of go-autogen being sourced in `hack/make.sh`. This re-adds
go-autogen to be sourced for test-integration-cli so that we can
actually run tests without the error found in:
https://github.com/moby/moby/pull/33857

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 3cdd471cac8193c34d8483255065c6c28a7b1645)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-21 14:41:26 -07:00
00313efde4 Merge pull request #123 from seemethere/cherry_pick_packaging_25
[17.07] Add ubuntu-xenial aarch64 Dockerfile
2017-07-20 16:35:08 -07:00
8bd577e801 Add ubuntu-xenial aarch64 Dockerfile
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
(cherry picked from commit 94cbc76b701b8b108f6a337b1695bd664f441bdc)
Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-20 16:33:08 -07:00
24244c1083 Merge pull request #122 from seemethere/fix_1707_version
[17.07.x] Increment engine version to 17.07.0-dev
2017-07-20 15:20:35 -07:00
4ca7fba8e3 Increment engine version to 17.07
Release jobs will fail if the version files are out of sync due to how
`hack/make.sh` builds binaries (putting them into the version folder
according to `components/engine/VERSION` instead of the base `VERSION`
file)

Signed-off-by: Eli Uriegas <eli.uriegas@docker.com>
2017-07-20 14:44:56 -07:00
1960 changed files with 61178 additions and 106605 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,125 +2,6 @@
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&mdash;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.

View File

@ -1 +1 @@
17.09.0-ce-rc3
17.07.0-ce-rc1

View File

@ -1,7 +1,6 @@
# 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

View File

@ -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&mdash;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.

View File

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

View File

@ -3,75 +3,64 @@
#
all: binary
_:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
# remove build artifacts
.PHONY: clean
clean: ## remove build artifacts
clean:
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/')
# run go test
# the "-tags daemon" part is temporary
.PHONY: test
test: test-unit ## run tests
test:
./scripts/test/unit $(shell go list ./... | grep -v '/vendor/')
.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
test-coverage:
./scripts/test/unit-with-coverage $(shell go list ./... | grep -v '/vendor/')
.PHONY: lint
lint: ## run all the lint tools
lint:
gometalinter --config gometalinter.json ./...
.PHONY: binary
binary: ## build executable for Linux
binary:
@echo "WARNING: binary creates a Linux executable. Use cross for macOS or Windows."
./scripts/build/binary
.PHONY: cross
cross: ## build executable for macOS and Windows
cross:
./scripts/build/cross
.PHONY: dynbinary
dynbinary: ## build dynamically linked binary
dynbinary:
./scripts/build/dynbinary
.PHONY: watch
watch: ## monitor file changes and run go test
watch:
./scripts/test/watch
vendor: vendor.conf ## check that vendor matches vendor.conf
# Check vendor matches vendor.conf
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
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
yamldocs:
scripts/docs/generate-yaml.sh
## Shellcheck validation
.PHONY: shellcheck
shellcheck: ## run shellcheck validation
shellcheck:
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)
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

View File

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

View File

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

View File

@ -1 +1 @@
17.09.0-ce-rc3
17.07.0-ce-rc1

View File

@ -71,8 +71,7 @@ jobs:
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'
curl -s https://codecov.io/bash | bash
validate:
working_directory: /work
@ -90,7 +89,7 @@ jobs:
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
make -B vendor compose-jsonschema manpages yamldocs
shellcheck:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
@ -104,7 +103,7 @@ jobs:
echo "COPY . ." >> $dockerfile
docker build -f $dockerfile --tag cli-validator:$CIRCLE_BUILD_NUM .
docker run --rm cli-validator:$CIRCLE_BUILD_NUM \
make shellcheck
make -B shellcheck
workflows:
version: 2
ci:

View File

@ -5,9 +5,9 @@ import (
"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/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -20,11 +20,11 @@ func TestCheckpointCreateErrors(t *testing.T) {
}{
{
args: []string{"too-few-arguments"},
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{"foo", "bar"},

View File

@ -1,13 +1,14 @@
package checkpoint
import (
"bytes"
"io/ioutil"
"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/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"
)
@ -36,9 +37,9 @@ func TestCheckpointListErrors(t *testing.T) {
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointListFunc: tc.checkpointListFunc,
})
}, &bytes.Buffer{})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -48,7 +49,8 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointListFunc: func(container string, options types.CheckpointListOptions) ([]types.Checkpoint, error) {
containerID = container
checkpointDir = options.CheckpointDir
@ -56,12 +58,14 @@ func TestCheckpointListWithOptions(t *testing.T) {
{Name: "checkpoint-foo"},
}, nil
},
})
}, buf)
cmd := newListCommand(cli)
cmd.SetArgs([]string{"container-foo"})
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NoError(t, cmd.Execute())
assert.Equal(t, "container-foo", containerID)
assert.Equal(t, "/dir/foo", checkpointDir)
golden.Assert(t, cli.OutBuffer().String(), "checkpoint-list-with-options.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "checkpoint-list-with-options.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,12 +1,13 @@
package checkpoint
import (
"bytes"
"io/ioutil"
"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/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -19,11 +20,11 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}{
{
args: []string{"too-few-arguments"},
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{"foo", "bar"},
@ -35,9 +36,9 @@ func TestCheckpointRemoveErrors(t *testing.T) {
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointDeleteFunc: tc.checkpointDeleteFunc,
})
}, &bytes.Buffer{})
cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -47,14 +48,14 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
checkpointDeleteFunc: func(container string, options types.CheckpointDeleteOptions) error {
containerID = container
checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir
return nil
},
})
}, &bytes.Buffer{})
cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
cmd.Flags().Set("checkpoint-dir", "/dir/foo")

View File

@ -2,11 +2,9 @@ package command
import (
"io"
"net"
"net/http"
"os"
"runtime"
"time"
"github.com/docker/cli/cli"
cliconfig "github.com/docker/cli/cli/config"
@ -216,10 +214,6 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
}
tr := &http.Transport{
TLSClientConfig: config,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
}
proto, addr, _, err := client.ParseHost(host)
if err != nil {

View File

@ -1,57 +0,0 @@
package command
import (
"os"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/flags"
"github.com/docker/docker/api"
"github.com/docker/docker/client"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
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: move to gotestyourself
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))
}
}

View File

@ -7,11 +7,11 @@ import (
"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 +26,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)},
@ -71,7 +71,8 @@ func TestConfigCreateWithName(t *testing.T) {
cmd := newConfigCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", configDataFile)})
assert.NoError(t, cmd.Execute())
golden.Assert(t, string(actual), configDataFile)
expected := golden.Get(t, actual, configDataFile)
assert.Equal(t, string(expected), string(actual))
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
}

View File

@ -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{
test.NewFakeCliWithOutput(&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.NewFakeCliWithOutput(&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.NewFakeCliWithOutput(&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.NewFakeCliWithOutput(&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))
}
}

View File

@ -6,14 +6,14 @@ import (
"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"
)
@ -68,7 +68,9 @@ func TestConfigList(t *testing.T) {
cmd := newConfigListCommand(cli)
cmd.SetOutput(cli.OutBuffer())
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list.golden")
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestConfigListWithQuietOption(t *testing.T) {
@ -85,7 +87,9 @@ func TestConfigListWithQuietOption(t *testing.T) {
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 := cli.OutBuffer().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) {
@ -104,7 +108,9 @@ func TestConfigListWithConfigFormat(t *testing.T) {
})
cmd := newConfigListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "config-list-with-config-format.golden")
actual := cli.OutBuffer().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) {
@ -121,7 +127,9 @@ func TestConfigListWithFormat(t *testing.T) {
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 := cli.OutBuffer().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) {
@ -149,5 +157,7 @@ func TestConfigListWithFilter(t *testing.T) {
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 := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "config-list-with-filter.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -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{
test.NewFakeCliWithOutput(&fakeClient{
configRemoveFunc: tc.configRemoveFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -43,25 +45,27 @@ 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{
cli := test.NewFakeCliWithOutput(&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{
cli := test.NewFakeCliWithOutput(&fakeClient{
configRemoveFunc: func(name string) error {
removedConfigs = append(removedConfigs, name)
if name == "foo" {
@ -69,7 +73,7 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
}
return nil
},
})
}, buf)
cmd := newConfigRemoveCommand(cli)
cmd.SetArgs(names)

View File

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

View File

@ -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": {

View File

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

View File

@ -1,2 +1,2 @@
foo
foo
bar label=label-bar

View File

@ -1,2 +1,2 @@
foo
foo
bar label=label-bar

View File

@ -4,13 +4,13 @@ import (
"io"
"net/http/httputil"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/signal"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -120,7 +120,18 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
}
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 +151,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
}

View File

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

View File

@ -1,73 +1,19 @@
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)
containerInspectFunc func(string) (types.ContainerJSON, error)
}
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if f.inspectFunc != nil {
return f.inspectFunc(containerID)
func (cli *fakeClient) ContainerInspect(_ context.Context, containerID string) (types.ContainerJSON, error) {
if cli.containerInspectFunc != nil {
return cli.containerInspectFunc(containerID)
}
return types.ContainerJSON{}, nil
}
func (f *fakeClient) ContainerExecCreate(_ context.Context, container string, config types.ExecConfig) (types.IDResponse, error) {
if f.execCreateFunc != nil {
return f.execCreateFunc(container, config)
}
return types.IDResponse{}, nil
}
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (types.ContainerExecInspect, error) {
if f.execInspectFunc != nil {
return f.execInspectFunc(execID)
}
return types.ContainerExecInspect{}, nil
}
func (f *fakeClient) ContainerExecStart(ctx context.Context, execID string, config types.ExecStartCheck) error {
return nil
}
func (f *fakeClient) ContainerCreate(
_ context.Context,
config *container.Config,
hostConfig *container.HostConfig,
networkingConfig *network.NetworkingConfig,
containerName string,
) (container.ContainerCreateCreatedBody, error) {
if f.createContainerFunc != nil {
return f.createContainerFunc(config, hostConfig, networkingConfig, containerName)
}
return container.ContainerCreateCreatedBody{}, nil
}
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options types.ImageCreateOptions) (io.ReadCloser, error) {
if f.imageCreateFunc != nil {
return f.imageCreateFunc(parentReference, options)
}
return nil, nil
}
func (f *fakeClient) Info(_ context.Context) (types.Info, error) {
if f.infoFunc != nil {
return f.infoFunc()
}
return types.Info{}, nil
}

View File

@ -113,9 +113,6 @@ type cidFile struct {
}
func (cid *cidFile) Close() error {
if cid.file == nil {
return nil
}
cid.file.Close()
if cid.written {
@ -129,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)
}
@ -140,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)
}
@ -162,15 +153,19 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
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 {
@ -198,7 +193,7 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerConfig
fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
// we don't want to write to stdout anything apart from container.ID
if err := pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil {
return nil, err
}
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
@ -220,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
}

View File

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

View File

@ -4,15 +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/docker/docker/pkg/promise"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
@ -24,13 +22,14 @@ type execOptions struct {
detach bool
user string
privileged bool
env opts.ListOpts
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`
@ -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,14 +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"})
return cmd
}
func runExec(dockerCli command.Cli, options execOptions) error {
execConfig := parseExec(options, dockerCli.ConfigFile())
// nolint: gocyclo
func runExec(dockerCli command.Cli, options *execOptions, container string, execCmd []string) error {
execConfig, err := parseExec(options, execCmd)
// just in case the ParseExec does not exit
if container == "" || err != nil {
return cli.StatusError{StatusCode: 1}
}
if options.detachKeys != "" {
dockerCli.ConfigFile().DetachKeys = options.detachKeys
}
// Send client escape keys
execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys
ctx := context.Background()
client := dockerCli.Client()
@ -72,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 {
@ -81,27 +93,27 @@ 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 {
// Interactive exec requested.
var (
out, stderr io.Writer
@ -123,7 +135,6 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execConfig *typ
}
}
client := dockerCli.Client()
resp, err := client.ContainerExecAttach(ctx, execID, *execConfig)
if err != nil {
return err
@ -154,35 +165,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(),
}
// If -d is not set, attach to everything by default
@ -194,10 +212,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
}

View File

@ -4,203 +4,120 @@ 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/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"github.com/stretchr/testify/require"
)
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)
require.NoError(t, 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 compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool {
if config1.AttachStderr != config2.AttachStderr {
return false
}
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
}
func TestNewExecCommandErrors(t *testing.T) {
testCases := []struct {
name string
@ -218,7 +135,7 @@ func TestNewExecCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{inspectFunc: tc.containerInspectFunc})
cli := test.NewFakeCli(&fakeClient{containerInspectFunc: tc.containerInspectFunc})
cmd := NewExecCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types/container"
@ -19,7 +20,6 @@ import (
"github.com/docker/docker/pkg/signal"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
)
@ -274,7 +274,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions {
// Low-level execution (cgroups, namespaces, ...)
flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container")
flags.StringVar(&copts.ipcMode, "ipc", "", "IPC 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")
@ -421,6 +421,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
return nil, err
}
ipcMode := container.IpcMode(copts.ipcMode)
if !ipcMode.Valid() {
return nil, errors.Errorf("--ipc: invalid IPC mode")
}
pidMode := container.PidMode(copts.pidMode)
if !pidMode.Valid() {
return nil, errors.Errorf("--pid: invalid PID mode")
@ -579,7 +584,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,

View File

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

View File

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

View File

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

View File

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

View File

@ -3,13 +3,14 @@ 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"
)
@ -124,6 +125,20 @@ func legacyWaitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli,
return statusChan
}
// getExitCode performs an inspect on the container. It returns
// the running state and the exit code.
func getExitCode(ctx context.Context, dockerCli command.Cli, containerID string) (bool, int, error) {
c, err := dockerCli.Client().ContainerInspect(ctx, containerID)
if err != nil {
// If we can't connect, then the daemon probably died.
if !clientapi.IsErrConnectionFailed(err) {
return false, -1, err
}
return false, -1, nil
}
return c.State.Running, c.State.ExitCode, nil
}
func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error {
if len(containers) == 0 {
return nil

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import (
"text/tabwriter"
"text/template"
"github.com/docker/cli/templates"
"github.com/docker/docker/pkg/templates"
"github.com/pkg/errors"
)

View File

@ -79,13 +79,10 @@ func (c *historyContext) ID() string {
}
func (c *historyContext) CreatedAt() string {
return time.Unix(c.h.Created, 0).Format(time.RFC3339)
return units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
}
func (c *historyContext) CreatedSince() string {
if !c.human {
return c.CreatedAt()
}
created := units.HumanDuration(time.Now().UTC().Sub(time.Unix(c.h.Created, 0)))
return created + " ago"
}

View File

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

View File

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

View File

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

View File

@ -1,104 +0,0 @@
package formatter
import (
"strconv"
"strings"
registry "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
)
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 = stringutils.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)
}

View File

@ -1,284 +0,0 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"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,
}, stringutils.Ellipsis(longDescription, 45), ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: false,
}, longDescription, ctx.Description},
{searchContext{
s: registrytypes.SearchResult{Description: descriptionWReturns},
trunc: true,
}, stringutils.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")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
result2 Not official 5 [OK]
`,
},
{
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")},
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED
result1 Official build 5000 [OK]
`,
},
{
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)
}
}

View File

@ -504,10 +504,7 @@ func (c *serviceContext) Replicas() string {
}
func (c *serviceContext) Image() string {
var image string
if c.service.Spec.TaskTemplate.ContainerSpec != nil {
image = c.service.Spec.TaskTemplate.ContainerSpec.Image
}
image := c.service.Spec.TaskTemplate.ContainerSpec.Image
if ref, err := reference.ParseNormalizedNamed(image); err == nil {
// update image string for display, (strips any digest)
if nt, ok := ref.(reference.NamedTagged); ok {
@ -521,11 +518,11 @@ func (c *serviceContext) Image() string {
}
func (c *serviceContext) Ports() string {
if c.service.Endpoint.Ports == nil {
if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil {
return ""
}
ports := []string{}
for _, pConfig := range c.service.Endpoint.Ports {
for _, pConfig := range c.service.Spec.EndpointSpec.Ports {
if pConfig.PublishMode == swarm.PortConfigPublishModeIngress {
ports = append(ports, fmt.Sprintf("*:%d->%d/%s",
pConfig.PublishedPort,

View File

@ -96,14 +96,14 @@ bar
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -112,14 +112,14 @@ bar
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -152,14 +152,14 @@ func TestServiceContextWriteJSON(t *testing.T) {
ID: "id_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},
@ -168,14 +168,14 @@ func TestServiceContextWriteJSON(t *testing.T) {
ID: "id_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
EndpointSpec: &swarm.EndpointSpec{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
},
},
},
},

View File

@ -5,7 +5,7 @@ import (
"github.com/docker/docker/api/types/swarm"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"

View File

@ -12,6 +12,7 @@ import (
"regexp"
"runtime"
"github.com/Sirupsen/logrus"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
@ -27,7 +28,6 @@ import (
"github.com/docker/docker/pkg/urlutil"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)

View File

@ -11,8 +11,8 @@ import (
"strings"
"testing"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

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

View File

@ -10,7 +10,7 @@ import (
"testing"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/stretchr/testify/assert"
@ -68,37 +68,3 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
sort.Strings(actual)
assert.Equal(t, []string{dockerfileName, ".dockerignore", "foo"}, actual)
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that build contexts
// starting with `github.com/` are special-cased, and the build command attempts
// to clone the remote repo.
func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
cmd := NewBuildCommand(test.NewFakeCli(nil))
cmd.SetArgs([]string{"github.com/docker/no-such-repository"})
cmd.SetOutput(ioutil.Discard)
err := cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "unable to prepare context: unable to 'git clone'")
}
// TestRunBuildFromLocalGitHubDirNonExistingRepo tests that a local directory
// starting with `github.com` takes precedence over the `github.com` special
// case.
func TestRunBuildFromLocalGitHubDir(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-build-from-local-dir-")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
buildDir := filepath.Join(tmpDir, "github.com", "docker", "no-such-repository")
err = os.MkdirAll(buildDir, 0777)
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0644)
require.NoError(t, err)
client := test.NewFakeCli(&fakeClient{})
cmd := NewBuildCommand(client)
cmd.SetArgs([]string{buildDir})
cmd.SetOutput(ioutil.Discard)
err = cmd.Execute()
require.NoError(t, err)
}

View File

@ -3,13 +3,14 @@ package image
import (
"fmt"
"io/ioutil"
"regexp"
"testing"
"time"
"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/image"
"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"
)
@ -24,7 +25,7 @@ func TestNewHistoryCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
},
{
name: "client-error",
@ -95,9 +96,11 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
assert.NoError(t, err)
actual := cli.OutBuffer().String()
if tc.outputRegex == "" {
golden.Assert(t, actual, fmt.Sprintf("history-command-success.%s.golden", tc.name))
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
} else {
assert.Regexp(t, tc.outputRegex, actual)
match, _ := regexp.MatchString(tc.outputRegex, actual)
assert.True(t, match)
}
}
}

View File

@ -1,14 +1,15 @@
package image
import (
"bytes"
"io"
"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/api/types"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -23,7 +24,7 @@ func TestNewImportCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
name: "import-failed",
@ -35,7 +36,8 @@ func TestNewImportCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
buf := new(bytes.Buffer)
cmd := NewImportCommand(test.NewFakeCliWithOutput(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -89,7 +91,8 @@ func TestNewImportCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}))
buf := new(bytes.Buffer)
cmd := NewImportCommand(test.NewFakeCliWithOutput(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())

View File

@ -1,14 +1,15 @@
package image
import (
"bytes"
"fmt"
"io/ioutil"
"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/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -21,11 +22,12 @@ func TestNewInspectCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
}
for _, tc := range testCases {
cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}))
buf := new(bytes.Buffer)
cmd := newInspectCommand(test.NewFakeCliWithOutput(&fakeClient{}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -76,13 +78,15 @@ func TestNewInspectCommandSuccess(t *testing.T) {
}
for _, tc := range testCases {
imageInspectInvocationCount = 0
cli := test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc})
cmd := newInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newInspectCommand(test.NewFakeCliWithOutput(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
assert.Equal(t, imageInspectInvocationCount, tc.imageCount)
}
}

View File

@ -1,15 +1,16 @@
package image
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/cli/config/configfile"
"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/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"
)
@ -24,7 +25,7 @@ func TestNewImagesCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{"arg1", "arg2"},
expectedError: "requires at most 1 argument.",
expectedError: "requires at most 1 argument(s).",
},
{
name: "failed-list",
@ -79,14 +80,17 @@ func TestNewImagesCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc})
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{imageListFunc: tc.imageListFunc}, buf)
cli.SetConfigFile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
cmd := NewImagesCommand(cli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("list-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("list-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -1,16 +1,17 @@
package image
import (
"bytes"
"fmt"
"io"
"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/api/types"
"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,7 +27,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{"arg"},
expectedError: "accepts no arguments.",
expectedError: "accepts no argument(s).",
},
{
name: "input-to-terminal",
@ -91,12 +92,14 @@ func TestNewLoadCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc})
cmd := NewLoadCommand(cli)
buf := new(bytes.Buffer)
cmd := NewLoadCommand(test.NewFakeCliWithOutput(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("load-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("load-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -1,15 +1,16 @@
package image
import (
"bytes"
"fmt"
"io/ioutil"
"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/filters"
"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"
)
@ -24,7 +25,7 @@ func TestNewPruneCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{"something"},
expectedError: "accepts no arguments.",
expectedError: "accepts no argument(s).",
},
{
name: "prune-error",
@ -36,9 +37,10 @@ func TestNewPruneCommandErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cmd := NewPruneCommand(test.NewFakeCliWithOutput(&fakeClient{
imagesPruneFunc: tc.imagesPruneFunc,
}))
}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -83,12 +85,16 @@ func TestNewPruneCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imagesPruneFunc: tc.imagesPruneFunc})
cmd := NewPruneCommand(cli)
buf := new(bytes.Buffer)
cmd := NewPruneCommand(test.NewFakeCliWithOutput(&fakeClient{
imagesPruneFunc: tc.imagesPruneFunc,
}, buf))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("prune-command-success.%s.golden", tc.name))
actual := buf.String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("prune-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -5,9 +5,9 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/gotestyourself/gotestyourself/golden"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/stretchr/testify/assert"
)
@ -19,7 +19,7 @@ func TestNewPullCommandErrors(t *testing.T) {
}{
{
name: "wrong-args",
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
args: []string{},
},
{
@ -68,6 +68,8 @@ func TestNewPullCommandSuccess(t *testing.T) {
cmd.SetArgs(tc.args)
err := cmd.Execute()
assert.NoError(t, err)
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("pull-command-success.%s.golden", tc.name))
actual := cli.OutBuffer().String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -6,9 +6,9 @@ import (
"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/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@ -23,7 +23,7 @@ func TestNewPushCommandErrors(t *testing.T) {
{
name: "wrong-args",
args: []string{},
expectedError: "requires exactly 1 argument.",
expectedError: "requires exactly 1 argument(s).",
},
{
name: "invalid-name",

View File

@ -5,10 +5,10 @@ import (
"io/ioutil"
"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/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"
)
@ -29,7 +29,7 @@ func TestNewRemoveCommandErrors(t *testing.T) {
}{
{
name: "wrong args",
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
name: "ImageRemove fail",
@ -96,14 +96,16 @@ func TestNewRemoveCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(cli)
fakeCli := test.NewFakeCli(&fakeClient{imageRemoveFunc: tc.imageRemoveFunc})
cmd := NewRemoveCommand(fakeCli)
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, cli.ErrBuffer().String())
assert.Equal(t, tc.expectedErrMsg, fakeCli.ErrBuffer().String())
}
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("remove-command-success.%s.golden", tc.name))
actual := fakeCli.OutBuffer().String()
expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:])
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
}
}

View File

@ -2,8 +2,6 @@ package image
import (
"io"
"os"
"path/filepath"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -43,10 +41,6 @@ func runSave(dockerCli command.Cli, opts saveOptions) error {
return errors.New("cowardly refusing to save to a terminal. Use the -o flag or redirect")
}
if err := validateOutputPath(opts.output); err != nil {
return errors.Wrap(err, "failed to save image")
}
responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images)
if err != nil {
return err
@ -60,13 +54,3 @@ func runSave(dockerCli command.Cli, opts saveOptions) error {
return command.CopyToFile(opts.output, responseBody)
}
func validateOutputPath(path string) error {
dir := filepath.Dir(path)
if dir != "" && dir != "." {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return errors.Errorf("unable to validate output path: directory %q does not exist", dir)
}
}
return nil
}

View File

@ -1,14 +1,15 @@
package image
import (
"bytes"
"io"
"io/ioutil"
"os"
"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"
"github.com/stretchr/testify/require"
@ -25,7 +26,7 @@ func TestNewSaveCommandErrors(t *testing.T) {
{
name: "wrong args",
args: []string{},
expectedError: "requires at least 1 argument.",
expectedError: "requires at least 1 argument(s).",
},
{
name: "output to terminal",
@ -42,11 +43,6 @@ func TestNewSaveCommandErrors(t *testing.T) {
return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("error saving image")
},
},
{
name: "output directory does not exist",
args: []string{"-o", "fakedir/out.tar", "arg1"},
expectedError: "failed to save image: unable to validate output path: directory \"fakedir\" does not exist",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc})
@ -89,11 +85,11 @@ func TestNewSaveCommandSuccess(t *testing.T) {
},
}
for _, tc := range testCases {
cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
cmd := NewSaveCommand(test.NewFakeCliWithOutput(&fakeClient{
imageSaveFunc: func(images []string) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
}))
}, new(bytes.Buffer)))
cmd.SetOutput(ioutil.Discard)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())

View File

@ -4,8 +4,8 @@ import (
"io/ioutil"
"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/stretchr/testify/assert"
)
@ -15,7 +15,7 @@ func TestCliNewTagCommandErrors(t *testing.T) {
{"image1"},
{"image1", "image2", "image3"},
}
expectedError := "\"tag\" requires exactly 2 arguments."
expectedError := "\"tag\" requires exactly 2 argument(s)."
for _, args := range testCases {
cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(args)

View File

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

View File

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

View File

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"github.com/docker/cli/templates"
"github.com/docker/docker/pkg/templates"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

View File

@ -2,16 +2,13 @@ package network
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"golang.org/x/net/context"
)
type fakeClient struct {
client.Client
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
networkCreateFunc func(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error)
}
func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options types.NetworkCreate) (types.NetworkCreateResponse, error) {
@ -20,17 +17,3 @@ func (c *fakeClient) NetworkCreate(ctx context.Context, name string, options typ
}
return types.NetworkCreateResponse{}, nil
}
func (c *fakeClient) NetworkConnect(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
if c.networkConnectFunc != nil {
return c.networkConnectFunc(ctx, networkID, container, config)
}
return nil
}
func (c *fakeClient) NetworkDisconnect(ctx context.Context, networkID, container string, force bool) error {
if c.networkDisconnectFunc != nil {
return c.networkDisconnectFunc(ctx, networkID, container, force)
}
return nil
}

View File

@ -1,70 +0,0 @@
package network
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/docker/api/types/network"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNetworkConnectErrors(t *testing.T) {
testCases := []struct {
args []string
networkConnectFunc func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error
expectedError string
}{
{
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"toto", "titi"},
networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
return errors.Errorf("error connecting network")
},
expectedError: "error connecting network",
},
}
for _, tc := range testCases {
cmd := newConnectCommand(
test.NewFakeCli(&fakeClient{
networkConnectFunc: tc.networkConnectFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestNetworkConnectWithFlags(t *testing.T) {
expectedOpts := []network.IPAMConfig{
{
"192.168.4.0/24",
"192.168.4.0/24",
"192.168.4.1/24",
map[string]string{},
},
}
cli := test.NewFakeCli(&fakeClient{
networkConnectFunc: func(ctx context.Context, networkID, container string, config *network.EndpointSettings) error {
assert.Equal(t, expectedOpts, config.IPAMConfig, "not expected driver error")
return nil
},
})
args := []string{"banana"}
cmd := newCreateCommand(cli)
cmd.SetArgs(args)
cmd.Flags().Set("driver", "foo")
cmd.Flags().Set("ip-range", "192.168.4.0/24")
cmd.Flags().Set("gateway", "192.168.4.1/24")
cmd.Flags().Set("subnet", "192.168.4.0/24")
assert.NoError(t, cmd.Execute())
}

View File

@ -5,10 +5,10 @@ import (
"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/network"
"github.com/docker/docker/pkg/testutil"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

View File

@ -1,41 +0,0 @@
package network
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
func TestNetworkDisconnectErrors(t *testing.T) {
testCases := []struct {
args []string
networkDisconnectFunc func(ctx context.Context, networkID, container string, force bool) error
expectedError string
}{
{
expectedError: "requires exactly 2 arguments",
},
{
args: []string{"toto", "titi"},
networkDisconnectFunc: func(ctx context.Context, networkID, container string, force bool) error {
return errors.Errorf("error disconnecting network")
},
expectedError: "error disconnecting network",
},
}
for _, tc := range testCases {
cmd := newDisconnectCommand(
test.NewFakeCli(&fakeClient{
networkDisconnectFunc: tc.networkDisconnectFunc,
}),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}

View File

@ -4,12 +4,12 @@ import (
"io/ioutil"
"testing"
"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/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)

View File

@ -1,18 +1,19 @@
package node
import (
"bytes"
"fmt"
"io/ioutil"
"testing"
"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"
)
@ -66,11 +67,12 @@ func TestNodeInspectErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newInspectCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
infoFunc: tc.infoFunc,
}))
}, buf))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
@ -107,13 +109,16 @@ func TestNodeInspectPretty(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
})
cmd := newInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
}, buf))
cmd.SetArgs([]string{"nodeID"})
cmd.Flags().Set("pretty", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -1,17 +1,19 @@
package node
import (
"bytes"
"io/ioutil"
"testing"
"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/gotestyourself/gotestyourself/golden"
"github.com/docker/docker/pkg/testutil"
"github.com/docker/docker/pkg/testutil/golden"
"github.com/pkg/errors"
// Import builders to get the builder function as package function
. "github.com/docker/cli/internal/test/builders"
. "github.com/docker/cli/cli/internal/test/builders"
"github.com/stretchr/testify/assert"
)
@ -56,9 +58,9 @@ func TestNodeList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1"), Hostname("node-2-foo"), Manager(Leader())),
*Node(NodeID("nodeID2"), Hostname("node-10-foo"), Manager()),
*Node(NodeID("nodeID3"), Hostname("node-1-foo")),
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
*Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()),
*Node(NodeID("nodeID3"), Hostname("nodeHostname3")),
}, nil
},
infoFunc: func() (types.Info, error) {
@ -72,25 +74,39 @@ func TestNodeList(t *testing.T) {
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "node-list-sort.golden")
out := cli.OutBuffer().String()
assert.Contains(t, out, `nodeID1 * nodeHostname1 Ready Active Leader`)
assert.Contains(t, out, `nodeID2 nodeHostname2 Ready Active Reachable`)
assert.Contains(t, out, `nodeID3 nodeHostname3 Ready Active`)
}
func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1")),
*Node(),
}, nil
},
})
}, buf)
cmd := newListCommand(cli)
cmd.Flags().Set("quiet", "true")
assert.NoError(t, cmd.Execute())
assert.Equal(t, cli.OutBuffer().String(), "nodeID1\n")
assert.Contains(t, buf.String(), "nodeID")
}
func TestNodeListDefaultFormatFromConfig(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
// Test case for #24090
func TestNodeListContainsHostname(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{}, buf)
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
assert.Contains(t, buf.String(), "HOSTNAME")
}
func TestNodeListDefaultFormat(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
@ -105,17 +121,20 @@ func TestNodeListDefaultFormatFromConfig(t *testing.T) {
},
}, nil
},
})
}, buf)
cli.SetConfigFile(&configfile.ConfigFile{
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
})
cmd := newListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "node-list-format-from-config.golden")
assert.Contains(t, buf.String(), `nodeID1: nodeHostname1 Ready/Leader`)
assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`)
assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`)
}
func TestNodeListFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())),
@ -129,12 +148,32 @@ func TestNodeListFormat(t *testing.T) {
},
}, nil
},
})
}, buf)
cli.SetConfigFile(&configfile.ConfigFile{
NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}",
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "node-list-format-flag.golden")
assert.Contains(t, buf.String(), `nodeHostname1: Leader`)
assert.Contains(t, buf.String(), `nodeHostname2: Reachable`)
}
func TestNodeListOrder(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
nodeListFunc: func() ([]swarm.Node, error) {
return []swarm.Node{
*Node(Hostname("node-2-foo"), Manager(Leader())),
*Node(Hostname("node-10-foo"), Manager()),
*Node(Hostname("node-1-foo")),
}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}")
assert.NoError(t, cmd.Execute())
actual := cli.OutBuffer().String()
expected := golden.Get(t, []byte(actual), "node-list-sort.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,15 +1,16 @@
package node
import (
"bytes"
"io/ioutil"
"testing"
"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/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)
@ -39,11 +40,12 @@ func TestNodePromoteErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newPromoteCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: tc.nodeInspectFunc,
nodeUpdateFunc: tc.nodeUpdateFunc,
}))
}, buf))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -51,8 +53,9 @@ func TestNodePromoteErrors(t *testing.T) {
}
func TestNodePromoteNoChange(t *testing.T) {
buf := new(bytes.Buffer)
cmd := newPromoteCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: func() (swarm.Node, []byte, error) {
return *Node(Manager()), []byte{}, nil
},
@ -62,14 +65,15 @@ func TestNodePromoteNoChange(t *testing.T) {
}
return nil
},
}))
}, buf))
cmd.SetArgs([]string{"nodeID"})
assert.NoError(t, cmd.Execute())
}
func TestNodePromoteMultipleNode(t *testing.T) {
buf := new(bytes.Buffer)
cmd := newPromoteCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeInspectFunc: func() (swarm.Node, []byte, error) {
return *Node(), []byte{}, nil
},
@ -79,7 +83,7 @@ func TestNodePromoteMultipleNode(t *testing.T) {
}
return nil
},
}))
}, buf))
cmd.SetArgs([]string{"nodeID1", "nodeID2"})
assert.NoError(t, cmd.Execute())
}

View File

@ -1,18 +1,20 @@
package node
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"
"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/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"
)
@ -48,13 +50,14 @@ func TestNodePsErrors(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
})
cmd := newPsCommand(cli)
buf := new(bytes.Buffer)
cmd := newPsCommand(
test.NewFakeCliWithOutput(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
}, buf))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
@ -111,18 +114,21 @@ func TestNodePs(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
})
cmd := newPsCommand(cli)
buf := new(bytes.Buffer)
cmd := newPsCommand(
test.NewFakeCliWithOutput(&fakeClient{
infoFunc: tc.infoFunc,
nodeInspectFunc: tc.nodeInspectFunc,
taskInspectFunc: tc.taskInspectFunc,
taskListFunc: tc.taskListFunc,
}, buf))
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("node-ps.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-ps.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -1,11 +1,12 @@
package node
import (
"bytes"
"io/ioutil"
"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"
)
@ -28,10 +29,11 @@ func TestNodeRemoveErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newRemoveCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
nodeRemoveFunc: tc.nodeRemoveFunc,
}))
}, buf))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
@ -39,7 +41,8 @@ func TestNodeRemoveErrors(t *testing.T) {
}
func TestNodeRemoveMultiple(t *testing.T) {
cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}))
buf := new(bytes.Buffer)
cmd := newRemoveCommand(test.NewFakeCliWithOutput(&fakeClient{}, buf))
cmd.SetArgs([]string{"nodeID1", "nodeID2"})
assert.NoError(t, cmd.Execute())
}

View File

@ -1,10 +1,10 @@
ID: nodeID
Name: defaultNodeName
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Status:
State: Ready
Availability: Active
Availability: Active
Address: 127.0.0.1
Manager Status:
Address: 127.0.0.1
@ -15,10 +15,11 @@ Platform:
Architecture: x86_64
Resources:
CPUs: 0
Memory: 20MiB
Memory: 20 MiB
Plugins:
Network: bridge, overlay
Volume: local
Network: bridge, overlay
Volume: local
Engine Version: 1.13.0
Engine Labels:
- engine=label
- engine = label

View File

@ -1,10 +1,10 @@
ID: nodeID
Name: defaultNodeName
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Status:
State: Ready
Availability: Active
Availability: Active
Address: 127.0.0.1
Manager Status:
Address: 127.0.0.1
@ -15,10 +15,11 @@ Platform:
Architecture: x86_64
Resources:
CPUs: 0
Memory: 20MiB
Memory: 20 MiB
Plugins:
Network: bridge, overlay
Volume: local
Network: bridge, overlay
Volume: local
Engine Version: 1.13.0
Engine Labels:
- engine=label
- engine = label

View File

@ -1,22 +1,23 @@
ID: nodeID
Name: defaultNodeName
Labels:
- lbl1=value1
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
- lbl1 = value1
Hostname: defaultNodeHostname
Joined at: 2009-11-10 23:00:00 +0000 utc
Status:
State: Ready
Availability: Active
Availability: Active
Address: 127.0.0.1
Platform:
Operating System: linux
Architecture: x86_64
Resources:
CPUs: 0
Memory: 20MiB
Memory: 20 MiB
Plugins:
Network: bridge, overlay
Volume: local
Network: bridge, overlay
Volume: local
Engine Version: 1.13.0
Engine Labels:
- engine=label
- engine = label

View File

@ -1,2 +0,0 @@
nodeHostname1: Leader
nodeHostname2: Reachable

View File

@ -1,3 +0,0 @@
nodeID1: nodeHostname1 Ready/Leader
nodeID2: nodeHostname2 Ready/Reachable
nodeID3: nodeHostname3 Ready/

View File

@ -1,4 +1,3 @@
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
nodeID3 node-1-foo Ready Active
nodeID1 * node-2-foo Ready Active Leader
nodeID2 node-10-foo Ready Active Reachable
node-1-foo:
node-2-foo: Leader
node-10-foo: Reachable

View File

@ -1,2 +1,2 @@
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp

View File

@ -1,4 +1,4 @@
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error"
taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error"
taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error"
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error"
taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error"
taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error"

View File

@ -4,12 +4,12 @@ import (
"io/ioutil"
"testing"
"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/docker/cli/cli/internal/test/builders"
"github.com/docker/docker/pkg/testutil"
"github.com/stretchr/testify/assert"
)

View File

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

View File

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

View File

@ -1,21 +1,23 @@
package registry
import (
"fmt"
"sort"
"strings"
"text/tabwriter"
"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"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/pkg/stringutils"
"github.com/docker/docker/registry"
"github.com/spf13/cobra"
"golang.org/x/net/context"
)
type searchOptions struct {
format string
term string
noTrunc bool
limit int
@ -45,7 +47,6 @@ func NewSearchCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.IntVar(&options.limit, "limit", registry.DefaultSearchLimit, "Max number of search results")
flags.StringVar(&options.format, "format", "", "Pretty-print search using a Go template")
flags.BoolVar(&options.automated, "automated", false, "Only show automated builds")
flags.UintVarP(&options.stars, "stars", "s", 0, "Only displays with at least x stars")
@ -88,12 +89,32 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
results := searchResultsByStars(unorderedResults)
sort.Sort(results)
searchCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewSearchFormat(options.format),
Trunc: !options.noTrunc,
w := tabwriter.NewWriter(dockerCli.Out(), 10, 1, 3, ' ', 0)
fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n")
for _, res := range results {
// --automated and -s, --stars are deprecated since Docker 1.12
if (options.automated && !res.IsAutomated) || (int(options.stars) > res.StarCount) {
continue
}
desc := strings.Replace(res.Description, "\n", " ", -1)
desc = strings.Replace(desc, "\r", " ", -1)
if !options.noTrunc {
desc = stringutils.Ellipsis(desc, 45)
}
fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount)
if res.IsOfficial {
fmt.Fprint(w, "[OK]")
}
fmt.Fprint(w, "\t")
if res.IsAutomated {
fmt.Fprint(w, "[OK]")
}
fmt.Fprint(w, "\n")
}
return formatter.SearchWrite(searchCtx, results, options.automated, int(options.stars))
w.Flush()
return nil
}
// searchResultsByStars sorts search results in descending order by number of stars.

View File

@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
// Prevents a circular import with "github.com/docker/cli/internal/test"
// Prevents a circular import with "github.com/docker/cli/cli/internal/test"
. "github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)

View File

@ -1,17 +1,18 @@
package secret
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 TestSecretCreateErrors(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", secretDataFile)},
@ -53,8 +54,9 @@ func TestSecretCreateErrors(t *testing.T) {
func TestSecretCreateWithName(t *testing.T) {
name := "foo"
buf := new(bytes.Buffer)
var actual []byte
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
if spec.Name != name {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
@ -66,13 +68,14 @@ func TestSecretCreateWithName(t *testing.T) {
ID: "ID-" + spec.Name,
}, nil
},
})
}, buf)
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)})
assert.NoError(t, cmd.Execute())
golden.Assert(t, string(actual), secretDataFile)
assert.Equal(t, "ID-"+name, strings.TrimSpace(cli.OutBuffer().String()))
expected := golden.Get(t, actual, secretDataFile)
assert.Equal(t, expected, actual)
assert.Equal(t, "ID-"+name, strings.TrimSpace(buf.String()))
}
func TestSecretCreateWithLabels(t *testing.T) {
@ -82,7 +85,8 @@ func TestSecretCreateWithLabels(t *testing.T) {
}
name := "foo"
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) {
if spec.Name != name {
return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
@ -96,12 +100,12 @@ func TestSecretCreateWithLabels(t *testing.T) {
ID: "ID-" + spec.Name,
}, nil
},
})
}, buf)
cmd := newSecretCreateCommand(cli)
cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)})
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()))
}

View File

@ -1,18 +1,19 @@
package secret
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 TestSecretInspectErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
@ -93,13 +95,17 @@ func TestSecretInspectWithoutFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
})
cmd := newSecretInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -129,14 +135,18 @@ func TestSecretInspectWithFormat(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
})
cmd := newSecretInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}, buf),
)
cmd.SetArgs(tc.args)
cmd.Flags().Set("format", tc.format)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}
@ -161,13 +171,16 @@ func TestSecretInspectPretty(t *testing.T) {
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
})
cmd := newSecretInspectCommand(cli)
buf := new(bytes.Buffer)
cmd := newSecretInspectCommand(
test.NewFakeCliWithOutput(&fakeClient{
secretInspectFunc: tc.secretInspectFunc,
}, buf))
cmd.SetArgs([]string{"secretID"})
cmd.Flags().Set("pretty", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), fmt.Sprintf("secret-inspect-pretty.%s.golden", tc.name))
actual := buf.String()
expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-pretty.%s.golden", tc.name))
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
}

View File

@ -7,14 +7,14 @@ import (
"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"
)
@ -36,10 +36,11 @@ func TestSecretListErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newSecretListCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: tc.secretListFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -49,7 +50,7 @@ func TestSecretListErrors(t *testing.T) {
func TestSecretList(t *testing.T) {
buf := new(bytes.Buffer)
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"),
@ -66,15 +67,18 @@ func TestSecretList(t *testing.T) {
),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.SetOutput(buf)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"), SecretName("foo")),
@ -83,15 +87,18 @@ func TestSecretListWithQuietOption(t *testing.T) {
})),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.Flags().Set("quiet", "true")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-quiet-option.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-quiet-option.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"), SecretName("foo")),
@ -100,17 +107,20 @@ func TestSecretListWithConfigFormat(t *testing.T) {
})),
}, nil
},
})
}, buf)
cli.SetConfigFile(&configfile.ConfigFile{
SecretFormat: "{{ .Name }} {{ .Labels }}",
})
cmd := newSecretListCommand(cli)
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-config-format.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-config-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
return []swarm.Secret{
*Secret(SecretID("ID-foo"), SecretName("foo")),
@ -119,15 +129,18 @@ func TestSecretListWithFormat(t *testing.T) {
})),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(cli)
cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}")
assert.NoError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "secret-list-with-format.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-format.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}
func TestSecretListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
buf := new(bytes.Buffer)
cli := test.NewFakeCliWithOutput(&fakeClient{
secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) {
assert.Equal(t, "foo", options.Filters.Get("name")[0], "foo")
assert.Equal(t, "lbl1=Label-bar", options.Filters.Get("label")[0])
@ -146,10 +159,12 @@ func TestSecretListWithFilter(t *testing.T) {
),
}, nil
},
})
}, buf)
cmd := newSecretListCommand(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(), "secret-list-with-filter.golden")
actual := buf.String()
expected := golden.Get(t, []byte(actual), "secret-list-with-filter.golden")
testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, string(expected))
}

View File

@ -1,12 +1,13 @@
package secret
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 TestSecretRemoveErrors(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 TestSecretRemoveErrors(t *testing.T) {
},
}
for _, tc := range testCases {
buf := new(bytes.Buffer)
cmd := newSecretRemoveCommand(
test.NewFakeCli(&fakeClient{
test.NewFakeCliWithOutput(&fakeClient{
secretRemoveFunc: tc.secretRemoveFunc,
}),
}, buf),
)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
@ -43,25 +45,27 @@ func TestSecretRemoveErrors(t *testing.T) {
func TestSecretRemoveWithName(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedSecrets []string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretRemoveFunc: func(name string) error {
removedSecrets = append(removedSecrets, name)
return nil
},
})
}, buf)
cmd := newSecretRemoveCommand(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, removedSecrets)
}
func TestSecretRemoveContinueAfterError(t *testing.T) {
names := []string{"foo", "bar"}
buf := new(bytes.Buffer)
var removedSecrets []string
cli := test.NewFakeCli(&fakeClient{
cli := test.NewFakeCliWithOutput(&fakeClient{
secretRemoveFunc: func(name string) error {
removedSecrets = append(removedSecrets, name)
if name == "foo" {
@ -69,7 +73,7 @@ func TestSecretRemoveContinueAfterError(t *testing.T) {
}
return nil
},
})
}, buf)
cmd := newSecretRemoveCommand(cli)
cmd.SetOutput(ioutil.Discard)

View File

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

View File

@ -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": {

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