Compare commits

..

172 Commits

Author SHA1 Message Date
08cf36daa6 Merge pull request #1303 from thaJeztah/remove_buildkit_experimental_annotations
Remove "experimental" annotations for buildkit
2018-08-22 00:57:27 +01:00
a500c394df Move "session" support out of experimental for API 1.39 and up
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-22 00:51:19 +02:00
60c75fda67 Remove "experimental" annotations for buildkit
BuildKit can now be enabled without the daemon having
experimental features enabled.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-22 00:38:59 +02:00
ca782599fb Merge pull request #1225 from thaJeztah/told_you_so_I_wont_warn_you_again
Use warnings provided by daemon
2018-08-21 13:42:16 -07:00
3c27ce21c9 Use warnings provided by daemon
Warnings are now generated by the daemon, and returned as
part of the /info API response.

If warnings are returned by the daemon; use those instead
of generating them locally.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-21 22:29:57 +02:00
7da71329bc bump docker/docker
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-21 22:29:43 +02:00
e97d004395 Merge pull request #1233 from selansen/default_addr_pool
Global Default Address Pool feature support
2018-08-21 14:55:24 -04:00
587a94c935 Global Default Address Pool feature support
This feature brings new attribute/option for swarm init command.
default-addr-pool will take string input which can be in below format.
"CIDR,CIDR,CIDR...:SUBNET-SIZE".
Signed-off-by: selansen <elango.siva@docker.com>
2018-08-21 14:34:00 -04:00
2461cd618d Merge pull request #1275 from AntaresS/buildkit-support
[enhancement] enable buildkit from daemon side
2018-08-20 16:15:32 -07:00
acf43b62b5 vendor dependency
Signed-off-by: Anda Xu <anda.xu@docker.com>
2018-08-20 15:32:25 -07:00
ef09ca8987 enable buildkit as builder from daemon; no env var needs to be set
Signed-off-by: Anda Xu <anda.xu@docker.com>
2018-08-20 11:59:39 -07:00
466e1b0741 Merge pull request #1302 from andrewhsu/venbk
vndr buildkit, containerd, console
2018-08-20 11:10:46 -07:00
7a73d112ff vndr buildkit, containerd, and console
vndr buildkit to e8c7acc
vndr containerd to v1.2.0-beta.0
vndr console to 4d8a41f

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-08-20 17:59:40 +00:00
0c444c521f Merge pull request #1260 from dhiltgen/ce_q3
Add CLI support for running dockerd in a container on containerd
2018-08-20 13:39:49 -04:00
fd2f1b3b66 Add engine commands built on containerd
This new collection of commands supports initializing a local
engine using containerd, updating that engine, and activating
the EE product

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-20 09:42:05 -07:00
11a312118f Vendoring bump for containerd and licensing
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-20 09:42:05 -07:00
3f7c6c8200 Merge pull request #1300 from mmacy/1299-trust-inspect-typo
Fix 'trust inspect' typo: "AdminstrativeKeys"
2018-08-20 10:08:45 +02:00
c11acddfb5 [WIP] fix trust inspect typo 'AdminstrativeKeys'
Signed-off-by: mmacy <marsma@microsoft.com>
2018-08-19 17:57:04 -07:00
5706f9518a Merge pull request #1297 from tiborvass/tls-update
vendor: update tlsconfig in go-connections to 7395e3f8aa162843a74ed6d48e79627d9792ac55
2018-08-19 14:46:46 -07:00
f472a1a480 Merge pull request #1296 from tiborvass/build-progress-flag-no-api-requirement
build: Remove API requirement for --progress as it is CLI only
2018-08-17 18:31:25 -07:00
b3d8c5deda Merge pull request #1295 from tiborvass/cmd-builder-prune-no-options
builder: Implement `builder prune` to prune build cache
2018-08-17 18:28:37 -07:00
8ae74b38d5 vendor: update tlsconfig in go-connections to 7395e3f8aa162843a74ed6d48e79627d9792ac55
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-08-17 17:53:22 +00:00
50f918801f build: Remove API requirement for --progress as it is CLI only
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-08-17 16:24:02 +00:00
cb142fa49f Merge pull request #1288 from tiborvass/build-secrets
Build --secret with buildkit
2018-08-17 17:20:32 +01:00
b4057f0293 vendor: Bump default API version to 1.39
vendors github.com/docker/docker to a7ff19d69a90dfe152abd146221c8b9b46a0903d

Signed-off-by: Tibor Vass <tibor@docker.com>
2018-08-17 15:52:11 +00:00
f597f2d026 Add new builder subcommand and implement builder prune to prune build cache.
This patch adds a new builder subcommand, allowing to add more builder-related
commands in the future. Unfortunately `build` expects an argument so could not
be used as a subcommand.

This also implements `docker builder prune`, which is needed to prune the builder
cache manually without having to call `docker system prune`.

Today when relying on the legacy builder, users are able to prune dangling images
(used as build cache) by running `docker image prune`. This patch allows the
same usecase with buildkit.

Signed-off-by: Tibor Vass <tibor@docker.com>
2018-08-17 15:18:18 +00:00
c4c4825591 build: implement build secrets with buildkit
This patch implements `docker build --secret id=mysecret,src=/secret/file`
for buildkit frontends that request the mysecret secret.

It is currently implemented in the tonistiigi/dockerfile:secrets20180808
frontend via RUN --mount=type=secret,id=mysecret

Signed-off-by: Tibor Vass <tibor@docker.com>
2018-08-17 14:01:32 +00:00
964173997d Merge pull request #1276 from tiborvass/buildkit-progress-flag
build: change --console=[auto,false,true] to --progress=[auto,plain,tty]
2018-08-15 20:42:47 -07:00
e92614a175 Merge pull request #1014 from AkihiroSuda/connhelper-sshonly
support SSH connection
2018-08-14 15:12:06 -07:00
4d4392ba04 Merge pull request #1284 from adshmh/refactor-stack-ps-tests
refactor stack ps tests
2018-08-14 18:59:56 +02:00
340e4ee8e5 refactor stack ps tests to table-driven
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-10 15:13:45 -04:00
3a3e720f91 Merge pull request #1283 from adshmh/refactor-trust-revoke-tests
refactor trust revoke command unit tests
2018-08-10 11:03:58 +02:00
560b0cd863 Merge pull request #1280 from adshmh/refactor-trust-inspect-tests
refactor trust inspect command unit tests
2018-08-10 10:37:53 +02:00
984d76b9dd refactored trust revoke command unit tests to use table-driven style
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-09 15:58:54 -04:00
6ef11c516d Merge pull request #1266 from adshmh/use-natural-sort-order-for-network-list
use sortorder library for sorting network list output
2018-08-09 12:05:55 +02:00
f8f0d72cf9 refactor trust inspect command unit tests to table-driven style
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-08 13:50:00 -04:00
5cc1f9006a use sortorder lib for sorting the output of volume list command
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-08 12:20:21 -04:00
04bb3c770f use sortorder lib for sorting in trust package
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-08 12:20:10 -04:00
a921313caf use sortorder lib for sorting network list output
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-08 12:06:59 -04:00
faeb8bb571 build: change --console=[auto,false,true] to --progress=[auto,plain,tty]
This changes the experimental --console flag to --progress following
feedback indicating avoidable confusion.

In addition to naming changes, the help output now has an additional
clarification, specifically: container output during builds are only
shown when progress output is set to plain. Not mentioning this was also
a big cause of confusion.

Signed-off-by: Tibor Vass <tibor@docker.com>
2018-08-07 18:18:13 +00:00
1d04f7d66b Merge pull request #1277 from vdemeester/template-subtests-followup
Migrate `TestExtractVariables` to subtests…
2018-08-07 16:30:11 +02:00
9cd7c1361c Migrate TestExtractVariables to subtests…
… as suggested in previous PR comment.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-08-07 16:06:13 +02:00
b5768bea9b Merge pull request #1271 from albers/completion-shellcheck
Fix shellcheck warnings
2018-08-04 01:36:36 +02:00
e8cd06c8eb Merge pull request #1269 from albers/completion-cp--archive
Add bash completion for `cp --archive`
2018-08-03 23:48:52 +02:00
ad28cec012 Merge pull request #1270 from Ace-Tang/master
bash completion: fix uncorrect completion
2018-08-03 23:26:09 +02:00
e587ec293b Fix shellcheck warnings
Signed-off-by: Harald Albers <github@albersweb.de>
2018-08-03 15:28:09 +02:00
7b4e2f3145 bash completion: fix uncorrect completion
fix uncorrect completion for command
docker docker <tab>

Signed-off-by: Ace-Tang <aceapril@126.com>
2018-08-03 21:27:27 +08:00
b9b3754ad3 Add bash completion for cp --archive
Signed-off-by: Harald Albers <github@albersweb.de>
2018-08-03 14:30:30 +02:00
e902ae9f84 Merge pull request #1259 from adshmh/add-unit-test-to-cover-network-list-sort
Add unit test to cover network list sort, refactor to table-driven tests
2018-08-02 20:28:53 +02:00
021bf39d76 refactor network list unit tests to table-driven style
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-02 14:07:06 -04:00
4f388ffca3 add unit test to cover the sort order of network list command
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-08-02 14:07:06 -04:00
ff1a34d9a9 Merge pull request #1262 from vdemeester/allow-custom-pattern
Allow custom pattern when extracting variable…
2018-08-02 18:00:38 +02:00
731b4f1fb4 Merge pull request #1254 from albers/completion-kubernetes
Add bash completion for kubernetes orchestrator
2018-08-02 17:30:31 +02:00
4c87725c35 Allow custom pattern when extracting variable…
… as it is possible to do it when interpolating. It also fixes when
there is 2 variables on the same *value* (in the composefile, on the
same line)

Finaly, renaming the default, used in cli, pattern to `defaultPattern`
to not be shadowed unintentionally.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-08-02 16:50:40 +02:00
08f8ee1320 Add bash completion for kubernetes orchestrator
Signed-off-by: Harald Albers <github@albersweb.de>
2018-08-02 13:54:31 +02:00
8ef01e869e Only complete swarm specific options with orchestrator=swarm
Signed-off-by: Harald Albers <github@albersweb.de>
2018-08-02 13:54:31 +02:00
ff953751d3 Add support for orchestrator specific bash completions
Signed-off-by: Harald Albers <github@albersweb.de>
2018-08-02 13:54:31 +02:00
4fbb009d39 Merge pull request #1251 from silvin-lubecki/fix-stack-help-command
Fix help message flags on docker stack commands and sub-commands
2018-08-02 13:10:36 +02:00
6f61cf053a support SSH connection
e.g. docker -H ssh://me@server

The `docker` CLI also needs to be installed on the remote host to
provide `docker system dial-stdio`, which proxies the daemon socket to stdio.

Please refer to docs/reference/commandline/dockerd.md .

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
2018-08-02 13:10:06 +09:00
261ff66d61 Merge pull request #1258 from wyckster/patch-2
Fixed wrong apostrophe character
2018-08-01 18:09:36 +02:00
b59823c784 Fixed wrong apostrophe character
Changed unexpected Unicode character 0x1fbf GREEK PSILI that was standing in as an imposter for an apostrophe:  an _impostrophe_.

Signed-off-by: Chad Faragher <wyckster@hotmail.com>
2018-08-01 11:06:07 -04:00
da544e8938 Merge pull request #1249 from vdemeester/compose-template-pkg-enhancement
Add a new `ExtractVariables` function to `compose/template` package
2018-08-01 16:18:46 +02:00
afb87e42f2 Add a new ExtractVariables function to compose/template package
It allows to get easily all the variables defined in a
composefile (the `map[string]interface{}` representation that
`loader.ParseYAML` returns at least) and their default value too.

This commit also does some small function extract on substitution
funcs to reduce a tiny bit duplication.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-08-01 16:12:49 +02:00
2093fd6e52 Merge pull request #1256 from silvin-lubecki/update-reviewers
Remove outdated completion reviewers file
2018-08-01 15:19:36 +02:00
9022a00fbe Remove outdated completion reviewers file
Clean maintainers and code owners files

Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2018-08-01 15:08:36 +02:00
21cce52b30 Fix help message flags on docker stack commands and sub-commands
PersistentPreRunE needs to be called within the help function to initialize all the flags (notably the orchestrator flag)
Add an e2e test as regression test

Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2018-08-01 01:48:27 +02:00
08f5f52cdc Merge pull request #1215 from albers/completion-service-logs-options
Add bash completion for `service logs --details|--raw`
2018-07-31 22:25:36 +02:00
24b7effa30 Merge pull request #1246 from vdemeester/bump-mergo
Bump mergo to v0.3.6
2018-07-31 10:59:41 -07:00
74071f2347 Merge pull request #1248 from marcov/add-help-target
Add `help` and remove `watch` targets in docker.Makefile
2018-07-31 16:52:07 +02:00
760ca04709 Add help and remove watch targets in docker.Makefile
* Add the `help` target to document make targets when building using a
container
* Remove the `watch` target (filewatcher was removed with c0588a9c) from
docker.Makefile and Makefile

Signed-off-by: Marco Vedovati <mvedovati@suse.com>
2018-07-31 14:38:51 +02:00
7f853fee87 Merge pull request #1247 from marcov/make-help
Allow running `make help` without out-of-container warning
2018-07-31 14:04:10 +02:00
265dec037b Allow running make help without out-of-container warning
Signed-off-by: Marco Vedovati <mvedovati@suse.com>
2018-07-31 10:56:48 +02:00
40650cfbd5 Merge pull request #1242 from cyphar/buildmode-pie
build: add -buildmode=pie
2018-07-31 10:09:11 +02:00
1f1507b0a4 Bump mergo to v0.3.6
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-31 09:57:56 +02:00
70d5cb0dd0 Merge pull request #1244 from vdemeester/expose-compose-transform
Exposes compose `loader.Transform` function…
2018-07-31 09:43:13 +02:00
0246bc1b3b Exposes compose loader.Transform function…
This should make it easier for people to write custom composefile
parser without duplicating too much code. It takes the default
transformers and any additional number of transformer for any
types. That way it's possible to transform a `cli/compose` map into a
custom type that would use some of `cli/compose` types and its own.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-31 09:37:09 +02:00
b4e50635a2 Merge pull request #1230 from vdemeester/update-k8s-and-other-deps
Update k8s and other deps
2018-07-30 17:49:11 +02:00
19653e7fad Merge pull request #1240 from thaJeztah/datapath_addr_api_annotation
Add API-version anotation for --data-path-addr
2018-07-30 17:47:52 +02:00
6cd0e2fe70 Merge pull request #1222 from thaJeztah/its_not_standard_but_its_in
Update --compose-file flag description to mention stdin
2018-07-30 16:23:29 +02:00
fffec04374 Re-order vendor.conf alphabeticaly
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-30 16:14:52 +02:00
ed335aba8c Merge pull request #990 from vdemeester/e2e-iddfile-from-moby
Import TestBuildIidFileSquash from moby to cli
2018-07-30 11:37:36 +02:00
164e812b7a build: add -buildmode=pie
Make all dynbinary builds be position-independent (this adds both
security benefits and can help with flaky builds on POWER
architectures).

Signed-off-by: Aleksa Sarai <asarai@suse.de>
2018-07-30 19:34:01 +10:00
91b1ad9d2b Add API-version anotation for --data-path-addr
This flag was added in Docker 17.06, API version 1.31 through
moby@8dc8cd4719f165c01c98e7d3ce1d6cea6a8f60b8, but didn't add
API-version annotations.

This patch adds the missing annotations to hide this flag if
the CLI is connected to an older version of the daemon that
doesn't support that API.

Before this patch:

    DOCKER_API_VERSION=1.30 docker swarm init --help | grep data-path-addr
          --data-path-addr string           Address or interface to use for data path traffic (format: <ip|interface>)

    DOCKER_API_VERSION=1.31 docker swarm init --help | grep data-path-addr
          --data-path-addr string           Address or interface to use for data path traffic (format: <ip|interface>)

With this patch applied:

    DOCKER_API_VERSION=1.30 docker swarm init --help | grep data-path-addr
    # (no result)

    DOCKER_API_VERSION=1.31 docker swarm init --help | grep data-path-addr
          --data-path-addr string           Address or interface to use for data path traffic (format: <ip|interface>)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-30 11:05:20 +02:00
edfd623594 Merge pull request #1237 from vdemeester/use-non-deprecated-filters-funcs
Migrate to non-deprecated functions of `api/types/filters`
2018-07-27 14:47:01 +00:00
55edeb497a Migrate to non-deprecated functions of api/types/filters
- Use `Contains` instead of `Include`
- Use `ToJSON` instead of `ToParam`
- Remove usage of `ParseFlag` as it is deprecated too

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-27 15:37:43 +02:00
a3464c0a20 Merge pull request #1140 from Vimal-Raghubir/1110-Fix-Warning-Message
Fix: Warning Message and Fallback search
2018-07-27 07:27:00 +00:00
b3b2ace735 Merge pull request #1235 from vdemeester/compose-add-missing-field
Add missing fields in compose/types
2018-07-26 15:20:42 +00:00
d13e2df65b Add missing fields in compose/types
Even though those fields are not supported by `docker stack deploy`
they are defined in versions `3.x` of compose schema, so the `compose`
package should be able to marshal/unmarshal them.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-26 17:04:22 +02:00
effe36a155 Merge pull request #1232 from tommilligan/1231-zsh-autocomplete-update-invalid
Fixed typo breaking zsh docker update autocomplete (closes #1231)
2018-07-25 14:17:57 +02:00
8788a4804f Bump some dependencies to more recent versions (and tagged if available)
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-25 14:16:41 +02:00
da00d1c49f Fixed typo breaking zsh docker update autocomplete
Signed-off-by: Tom Milligan <tommilligan@users.noreply.github.com>
2018-07-25 12:23:08 +01:00
c8f0e211b9 Bump kubernetes dependencies to 1.11
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-25 11:34:59 +02:00
601131634e Merge pull request #1227 from pszczekutowicz/master
Added events filter scope for bash and zsh completion
2018-07-22 22:33:25 +02:00
13db1bc95f Added events filter scope zsh completion
Signed-off-by: Paweł Szczekutowicz <pszczekutowicz@gmail.com>
2018-07-20 15:47:00 +02:00
c922ea2f45 Added events filter scope bash completion
Signed-off-by: Paweł Szczekutowicz <pszczekutowicz@gmail.com>
2018-07-20 15:37:49 +02:00
543d6fb8da Merge pull request #1226 from thaJeztah/harder_better_slower_stronger
Bump version to 18.09.0-dev
2018-07-20 10:30:41 +02:00
7fba38acad Bump version to 18.09.0-dev
Releases will be supported for a longer term, and
will be done on a 6-month(ish) cycle.

https://blog.docker.com/2018/07/extending-support-cycle-docker-community-edition/

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-20 10:14:32 +02:00
2c7822b036 Update --compose-file flag description to mention stdin
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-19 14:55:48 +02:00
48fbb12b7c Merge pull request #1216 from justyntemme/patch-4
Update deploy.go
2018-07-17 16:20:46 +02:00
bdd58a4096 Update deploy.go
Clarified ambiguous error message

Update kubernetes/cli.go

Infromed user of why the error was caused when file is not there

Signed-off-by: Justyn Temme <justyntemme@gmail.com>
2018-07-17 09:14:05 -05:00
4912846de2 Add bash completion for service logs --details|--raw
Signed-off-by: Harald Albers <github@albersweb.de>
2018-07-16 18:46:06 +02:00
9e71207327 Merge pull request #1019 from ktomk/fix-env-file
import environment variables that are present
2018-07-16 13:41:29 +02:00
7b82276c88 Merge pull request #1192 from peter-kehl/master
For docker/docker.github.io/issues/6987
2018-07-16 12:22:02 +02:00
b395d2d6f5 Merge pull request #1212 from thaJeztah/bump_circleci_docker
Update CircleCI Docker version to 18.03
2018-07-13 15:13:40 +02:00
ce3d069936 Fix: Warning Message
Signed-off-by: Vimal-Raghubir <vraghubir0418@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-13 13:39:24 +02:00
13eb2aa125 Configure CircleCI remote daemon to use version 18.03.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-13 11:51:41 +02:00
d57cc1782e Merge pull request #1208 from thaJeztah/update-linting
Update gometalinter to v2.0.6, remove nakedret
2018-07-13 11:41:59 +02:00
e2a56c47da Update CircleCI Docker version to 18.03
17.06 has reached EOL a long time ago; let's use a current
version in CI as well :D

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-13 11:26:07 +02:00
9847e96765 Update hints for linting
- remove some hints that are no longer needed
- added a nolint: unparam for removeSingleSigner() (return bool is only used in tests)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-13 11:18:50 +02:00
f3811e865e Update gometalinter to v2.0.6 and remove alexkohler/nakedret
alexkohler/nakedret is now installed by default with gometalinter,
so it's no longer needed to install this manually

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-13 11:17:43 +02:00
d97f378009 Merge pull request #1210 from albers/completion-service-create--init
Add bash completion for `service create|update --init`
2018-07-13 09:44:11 +02:00
97d312e02a Add bash completion for service create|update --init
Signed-off-by: Harald Albers <github@albersweb.de>
2018-07-13 09:05:29 +02:00
ee8cdb3850 Merge pull request #1204 from thaJeztah/improve-version-align
Improve version output alignment
2018-07-12 10:53:50 +02:00
0f7ae34ea9 Adapt min-column width to component information
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-12 01:34:34 +02:00
55ff66d967 Extend version-align test with components
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-12 01:31:23 +02:00
c8b9c21ef9 Merge pull request #1178 from cyli/fix-swarm-ca-command
Propagate the provided external CA certificate to the external CA object in swarm
2018-07-10 01:31:32 +02:00
b91953f507 Merge pull request #1196 from adshmh/use-sort-slice-for-sorting-cli-compose
refactored commands to use sort.Slice
2018-07-09 07:18:11 -07:00
71d650ee17 refactored cli/compose and cli/command/trust to use sort.Slice and removed custom types used for sorting
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-07-08 15:08:17 -04:00
2b221d8f1c Merge pull request #1194 from adshmh/use-sort-slice-for-sorting-output
use sort.Slice for sorting commands' output
2018-07-07 18:13:12 +02:00
ceed42217d refactored all commands under cli/command/ to use sort.Slice instead of declaring custom types for sorting
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-07-06 15:49:32 -04:00
2634562119 Merge pull request #1102 from sfluor/1074-fix-mapping-a-range-of-host-ports-to-a-single-container-port
Fix mapping a range of host ports to a single container port
2018-07-06 14:41:00 +02:00
8160759013 Merge pull request #1166 from adshmh/add-sort-to-plugin-list
Sort plugin names in a natural order
2018-07-05 16:34:25 -07:00
249c8652a2 For docker/docker.github.io/issues/6987
Signed-off-by: Peter Kehl <peter.kehl@gmail.com>
2018-07-05 14:12:47 -07:00
26151d910a The output of plugin list command is sorted by plugin name
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-07-05 12:35:53 -04:00
29612ccefe Adding support of the long syntax publish notation
Signed-off-by: Sami Tabet <salph.tabet@gmail.com>
2018-07-05 00:33:13 +02:00
f285fe67e9 Merge pull request #1163 from thaJeztah/bump_engine
bump docker and dependencies
2018-07-04 16:17:12 +02:00
5f6d5c7328 Bump docker and dependencies
Updates docker/docker to 1436dc8f8d0f6f60b6e335fbd918d6b22ee6574d,
matching 18.06.0-rc1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-07-04 01:14:40 +00:00
bded5beb78 Merge pull request #1186 from tiborvass/buildkit-envvar-zero
build: use strconv.ParseBool to parse DOCKER_BUILDKIT to allow value "0"
2018-07-04 01:28:03 +02:00
721000e6c9 build: use strconv.ParseBool to parse DOCKER_BUILDKIT to allow value "0"
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-07-03 23:14:06 +00:00
7b255e653a Merge pull request #1176 from tiborvass/buildkit-iidfile
build: --iidfile support with buildkit
2018-07-03 15:36:23 -07:00
c7e85c09d2 build: --iidfile support with buildkit
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-07-03 19:11:11 +00:00
69e1743e3d Merge pull request #1156 from dmcgowan/fix-manifest-list-size
Fix manifest lists to always use correct size
2018-07-03 11:27:19 -07:00
4243440e1f Propagate the provided external CA certificate to the external CA object
in swarm.

Also, fix some CLI command confusions:
1. If the --external-ca flag is provided, require a --ca-cert flag as well, otherwise
   the external CA is set but the CA certificate is actually rotated to an internal
   cert
2. If a --ca-cert flag is provided, require a --ca-key or --external-ca flag be
   provided as well, otherwise either the server will say that the request is
   invalid, or if there was previously an external CA corresponding to the cert, it
   will succeed.  While that works, it's better to require the user to explicitly
   set all the parameters of the new desired root CA.

This also changes the `swarm update` function to set the external CA's CACert field,
which while not strictly necessary, makes the CA list more explicit.

Signed-off-by: Ying Li <ying.li@docker.com>
2018-07-02 17:14:21 -07:00
f5393c904a Merge pull request #1175 from vdemeester/bump-k8s
Bump kubernetes dependencies to 1.8.14
2018-07-02 17:12:35 +02:00
b59c41b2a7 Bump kubernetes dependencies to 1.8.14
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-02 16:32:07 +02:00
95a9b4d5fe Merge pull request #1172 from vdemeester/no-need-to-check-files
Remove composefiles length check on k8s RunDeploy
2018-07-02 15:06:50 +02:00
847e0c22d4 Remove composefiles lenght check on k8s RunDeploy..
The compose file(s) are already loaded at that point.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-02 15:00:19 +02:00
a2b4d30cd0 Merge pull request #1171 from silvin-lubecki/fix-doc-typo
Fix Format example typo"
2018-07-02 14:47:39 +02:00
d0ddf91539 Fixing issue #1167 "Format example typo"
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2018-07-02 14:21:10 +02:00
18091ea7e2 Merge pull request #1170 from vdemeester/omit-silvin
Add omitempty on compose config top-level types
2018-07-02 14:16:53 +02:00
f05ab2b1fb Add omitempty on compose config top-level types
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-02 12:04:09 +02:00
981c099b96 Merge pull request #1169 from silvin-lubecki/schema-vendoring
Add a doc.go file so the compose/schema/data directory can be vendore…
2018-07-02 10:47:25 +02:00
1c69e83034 Merge pull request #1168 from vdemeester/update-testing
Update TESTING.md to replace testify by gotest.tools
2018-07-02 10:34:28 +02:00
3a8ef767f8 Add a doc.go file so the compose/schema/data directory can be vendored in another project, without being pruned.
Signed-off-by: Silvin Lubecki <silvin.lubecki@docker.com>
2018-07-02 10:08:25 +02:00
9e36ff4491 Merge pull request #1160 from euank/simpler-pass
config/credentials: don't run 'pass' to detect it
2018-07-02 09:40:47 +02:00
057bf6f4d1 Update TESTING.md to replace testify by gotest.tools
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-07-02 09:24:28 +02:00
b91fd12996 add test for zero length variable name
parsing an environment file should give an error in case a zero-length
variable name (definition w/o a variable name) is encountered.

previously these lines went through unnoticed not informing the user about
a potential configuration error.

Signed-off-by: Tom Klingenberg <tklingenberg@lastflood.net>
2018-07-02 07:52:02 +02:00
96c026eb30 import environment variables that are present
previously docker did import environment variables if they were present
but created them if they were not when it was asked via a --env-file
cli option to import but not create them.

fix is to only import the variable into the environment if it is present.

additionally do not import variable names of zero-length (which are lines
w/ a potential variable definition w/o a variable name).

refs:

- https://github.com/docker/for-linux/issues/284

Signed-off-by: Tom Klingenberg <tklingenberg@lastflood.net>
2018-07-02 07:37:12 +02:00
1e89745704 add test for undefined variable environment file import
test to show current behavior is wrong at parsing an environment file
defining an undefined variable - it must not be defined!

NOTE: this test assume the $HOME variable is always set (see POSIX, this
      normally is the case, e.g. the test suite remains stable).

Signed-off-by: Tom Klingenberg <tklingenberg@lastflood.net>
2018-07-02 07:33:44 +02:00
34ba66b0c5 Merge pull request #1157 from AzureCR/master
Updated the go-winio library to release 0.4.8 that has the fix for Windows Container
2018-06-29 21:11:53 +02:00
056015c3d8 config/credentials: don't run 'pass' to detect it
'CheckInitialized' in the credential-helper library actually invokes
`pass`, which isn't desirable (see #699).

This moves the check to be simpler, and then pass will only be invoked
when it's needed (such as for `docker login` or when pulling from a
private registry).

This logic could also reasonably live in the credential-helper library,
but it's simple enough it seems fine in either location.

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-29 11:38:39 -07:00
c98c4080a3 Updated the go-winio library to release 0.4.8 that has the fix for Windows containers
Signed-off-by: Tejaswini Duggaraju <naduggar@microsoft.com>
2018-06-29 10:49:52 -07:00
da59ccb601 Merge pull request #1145 from Vimal-Raghubir/590-Add-missing-option
Add: Add missing option
2018-06-29 17:16:25 +02:00
9faf728089 Merge pull request #1155 from adshmh/add-unit-tests-to-plugin-list
added unit tests to cover plugin list command
2018-06-29 15:49:30 +02:00
7c7c299eee Merge pull request #1152 from vdemeester/extract-converter
Extract StackConverter from the StackClient
2018-06-29 13:36:55 +02:00
3991b2fae3 Merge pull request #1158 from albers/completion-dockerd--default-address-pool
Add bash completion for `dockerd --default-address-pool`
2018-06-29 11:48:43 +02:00
fe7ec42566 Merge pull request #1159 from albers/completion-events-exec_die
Add bash completion for `exec_die` event
2018-06-29 11:47:58 +02:00
8443982188 Add bash completion for exec_die event
Signed-off-by: Harald Albers <github@albersweb.de>
2018-06-29 11:20:23 +02:00
0e6d9dfe85 Add bash completion for dockerd --default-address-pool
Signed-off-by: Harald Albers <github@albersweb.de>
2018-06-29 11:11:24 +02:00
1fd2d66df8 Fix manifest lists to always use correct size
Stores complete OCI descriptor instead of digest and platform
fields. This includes the size which was getting lost by not
storing the original manifest bytes.

Attempt to support existing cached files, if not output
the filename with the incorrect content.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2018-06-28 18:17:38 -07:00
c26121df5c added unit tests to cover plugin list command
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-06-28 16:51:54 -04:00
ea65e9043c Merge pull request #1154 from thaJeztah/bump_version_18.07_dev
Bump version to 18.07.0-dev
2018-06-28 16:24:08 +02:00
f1fa1f3f15 Bump version to 18.07.0-dev
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-06-28 15:44:15 +02:00
293553944d Merge pull request #1151 from vdemeester/update-docker-credential-helper-pass
Update docker-credential-helpers dependency
2018-06-28 14:57:05 +02:00
d9741fc96b Update docker-credential-helpers dependency
This is mainly for the `pass` helper ; `pass` shouldn't be called
every docker command anymore ;).

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-06-28 14:44:53 +02:00
b21f9dde61 Merge pull request #1149 from adshmh/add-unit-tests-to-plugin-install
added unit tests covering content trust for plugin install command
2018-06-28 09:28:18 +02:00
bc9b42ea9b added unit tests covering content trust for plugin install command
Signed-off-by: Arash Deshmeh <adeshmeh@ca.ibm.com>
2018-06-27 22:16:19 -04:00
f2e6ee6899 Extract StackConverter from the StackClient
It makes it easier to get the correct stack from a compose config
struct without requiring the client (and thus talking to k8s API)

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-06-27 16:41:00 +02:00
a205aecb80 Add: Add missing option
Signed-off-by: Vimal-Raghubir <vraghubir0418@gmail.com>
2018-06-26 22:20:01 -04:00
a522a78231 Make test-e2e run against experimental and non-experimental daemon
- `make test-e2e` runs the e2e tests twice : once against on
  non-experimental daemon (as before), once against an experimental
  daemon.
- adds `test-e2e-experimental` and `test-e2e-non-experimental` target
  to run tests for the specified cases

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-06-25 11:46:35 +02:00
0e83042e54 Import TestBuildIidFileSquash from moby to cli
It's a cli only feature so the test belongs to the cli.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-06-25 11:44:09 +02:00
63e5c29e00 Fix mapping a range of host ports to a single container port
Signed-off-by: Sami Tabet <salph.tabet@gmail.com>
2018-06-12 11:54:02 +02:00
1700 changed files with 215511 additions and 67007 deletions

View File

@ -12,14 +12,14 @@ clean: ## remove build artifacts
.PHONY: test-unit
test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/')
.PHONY: test
test: test-unit ## run tests
.PHONY: test-coverage
test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/')
.PHONY: lint
lint: ## run all the lint tools
@ -46,10 +46,6 @@ binary-osx: ## build executable for macOS
dynbinary: ## build dynamically linked binary
./scripts/build/dynbinary
.PHONY: watch
watch: ## monitor file changes and run go test
./scripts/test/watch
vendor: vendor.conf ## check that vendor matches vendor.conf
rm -rf vendor
bash -c 'vndr |& grep -v -i clone'

View File

@ -26,13 +26,11 @@ Test<Function Name><Test Case Name>
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).
[gotest.tools/assert](https://godoc.org/gotest.tools/assert).
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).
[gotest.tools](https://godoc.org/gotest.tools).
## End-to-End Test Suite

View File

@ -1 +1 @@
18.06.0-dev
18.09.0-dev

View File

@ -4,7 +4,7 @@ clone_folder: c:\gopath\src\github.com\docker\cli
environment:
GOPATH: c:\gopath
GOVERSION: 1.10.4
GOVERSION: 1.10.3
DEPVERSION: v0.4.1
install:

View File

@ -4,10 +4,11 @@ jobs:
lint:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
docker: [{image: 'docker:18.03-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@ -22,11 +23,12 @@ jobs:
cross:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
docker: [{image: 'docker:18.03-git'}]
parallelism: 3
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@ -48,10 +50,11 @@ jobs:
test:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
docker: [{image: 'docker:18.03-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@ -76,10 +79,11 @@ jobs:
validate:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
docker: [{image: 'docker:18.03-git'}]
steps:
- checkout
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
@ -93,10 +97,13 @@ jobs:
make ci-validate
shellcheck:
working_directory: /work
docker: [{image: 'docker:17.06-git'}]
docker: [{image: 'docker:18.03-git'}]
steps:
- checkout
- setup_remote_docker
- setup_remote_docker:
version: 18.03.1-ce
reusable: true
exclusive: false
- run:
name: "Run shellcheck"
command: |

View File

@ -0,0 +1,22 @@
package builder
import (
"github.com/spf13/cobra"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
)
// NewBuilderCommand returns a cobra command for `builder` subcommands
func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "builder",
Short: "Manage builds",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
NewPruneCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,31 @@
package builder
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
// NewPruneCommand returns a new cobra prune command for images
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "prune",
Short: "Remove build cache",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
report, err := dockerCli.Client().BuildCachePrune(context.Background())
if err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(report.SpaceReclaimed)))
return nil
},
Annotations: map[string]string{"version": "1.39"},
}
return cmd
}

View File

@ -14,10 +14,12 @@ import (
"github.com/docker/cli/cli/config"
cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/connhelper"
cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/containerizedengine"
dopts "github.com/docker/cli/opts"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
@ -53,6 +55,7 @@ type Cli interface {
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error)
}
// DockerCli is an instance the docker command line client.
@ -205,6 +208,7 @@ func (cli *DockerCli) initializeFromClient() {
cli.serverInfo = ServerInfo{
HasExperimental: ping.Experimental,
OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion,
}
cli.client.NegotiateAPIVersionPing(ping)
}
@ -228,11 +232,17 @@ func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
}
// NewContainerizedEngineClient returns a containerized engine client
func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) {
return containerizedengine.NewClient(sockPath)
}
// ServerInfo stores details about the supported features and platform of the
// server
type ServerInfo struct {
HasExperimental bool
OSType string
BuildkitVersion types.BuilderVersion
}
// ClientInfo stores details about the supported features of the client
@ -248,31 +258,54 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerC
// NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
host, err := getServerHost(opts.Hosts, opts.TLSOptions)
unparsedHost, err := getUnparsedServerHost(opts.Hosts)
if err != nil {
return &client.Client{}, err
}
var clientOpts []func(*client.Client) error
helper, err := connhelper.GetConnectionHelper(unparsedHost)
if err != nil {
return &client.Client{}, err
}
if helper == nil {
clientOpts = append(clientOpts, withHTTPClient(opts.TLSOptions))
host, err := dopts.ParseHost(opts.TLSOptions != nil, unparsedHost)
if err != nil {
return &client.Client{}, err
}
clientOpts = append(clientOpts, client.WithHost(host))
} else {
clientOpts = append(clientOpts, func(c *client.Client) error {
httpClient := &http.Client{
// No tls
// No proxy
Transport: &http.Transport{
DialContext: helper.Dialer,
},
}
return client.WithHTTPClient(httpClient)(c)
})
clientOpts = append(clientOpts, client.WithHost(helper.Host))
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
}
customHeaders := configFile.HTTPHeaders
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = UserAgent()
clientOpts = append(clientOpts, client.WithHTTPHeaders(customHeaders))
verStr := api.DefaultVersion
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
verStr = tmpStr
}
clientOpts = append(clientOpts, client.WithVersion(verStr))
return client.NewClientWithOpts(
withHTTPClient(opts.TLSOptions),
client.WithHTTPHeaders(customHeaders),
client.WithVersion(verStr),
client.WithHost(host),
)
return client.NewClientWithOpts(clientOpts...)
}
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
func getUnparsedServerHost(hosts []string) (string, error) {
var host string
switch len(hosts) {
case 0:
@ -282,8 +315,7 @@ func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error
default:
return "", errors.New("Please specify only one -H")
}
return dopts.ParseHost(tlsOptions != nil, host)
return host, nil
}
func withHTTPClient(tlsOpts *tlsconfig.Options) func(*client.Client) error {

View File

@ -4,9 +4,11 @@ import (
"os"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/builder"
"github.com/docker/cli/cli/command/checkpoint"
"github.com/docker/cli/cli/command/config"
"github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/engine"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/manifest"
"github.com/docker/cli/cli/command/network"
@ -40,6 +42,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
image.NewImageCommand(dockerCli),
image.NewBuildCommand(dockerCli),
// builder
builder.NewBuilderCommand(dockerCli),
// manifest
manifest.NewManifestCommand(dockerCli),
@ -80,6 +85,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
// volume
volume.NewVolumeCommand(dockerCli),
// engine
engine.NewEngineCommand(dockerCli),
// legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)),

View File

@ -9,19 +9,10 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type byConfigName []swarm.Config
func (r byConfigName) Len() int { return len(r) }
func (r byConfigName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byConfigName) Less(i, j int) bool {
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
}
type listOptions struct {
quiet bool
format string
@ -67,7 +58,9 @@ func runConfigList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Sort(byConfigName(configs))
sort.Slice(configs, func(i, j int) bool {
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
})
configCtx := formatter.Context{
Output: dockerCli.Out(),

View File

@ -370,9 +370,24 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
entrypoint = []string{""}
}
ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll())
publishOpts := copts.publish.GetAll()
var ports map[nat.Port]struct{}
var portBindings map[nat.Port][]nat.PortBinding
ports, portBindings, err = nat.ParsePortSpecs(publishOpts)
// If simple port parsing fails try to parse as long format
if err != nil {
return nil, err
publishOpts, err = parsePortOpts(publishOpts)
if err != nil {
return nil, err
}
ports, portBindings, err = nat.ParsePortSpecs(publishOpts)
if err != nil {
return nil, err
}
}
// Merge in exposed ports to the map of published ports
@ -661,6 +676,23 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err
}, nil
}
func parsePortOpts(publishOpts []string) ([]string, error) {
optsList := []string{}
for _, publish := range publishOpts {
params := map[string]string{"protocol": "tcp"}
for _, param := range strings.Split(publish, ",") {
opt := strings.Split(param, "=")
if len(opt) < 2 {
return optsList, errors.Errorf("invalid publish opts format (should be name=value but got '%s')", param)
}
params[opt[0]] = opt[1]
}
optsList = append(optsList, fmt.Sprintf("%s:%s/%s", params["target"], params["published"], params["protocol"]))
}
return optsList, nil
}
func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) {
loggingOptsMap := opts.ConvertKVStringsToMap(loggingOpts)
if loggingDriver == "none" && len(loggingOpts) > 0 {

View File

@ -42,7 +42,6 @@ func TestValidateAttach(t *testing.T) {
}
}
// nolint: unparam
func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) {
flags, copts := setupRunFlags()
if err := flags.Parse(args); err != nil {

View File

@ -0,0 +1,181 @@
package engine
import (
"context"
"fmt"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/cli/internal/licenseutils"
"github.com/docker/docker/api/types"
"github.com/docker/licensing/model"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type activateOptions struct {
licenseFile string
version string
registryPrefix string
format string
image string
quiet bool
displayOnly bool
sockPath string
}
// newActivateCommand creates a new `docker engine activate` command
func newActivateCommand(dockerCli command.Cli) *cobra.Command {
var options activateOptions
cmd := &cobra.Command{
Use: "activate [OPTIONS]",
Short: "Activate Enterprise Edition",
Long: `Activate Enterprise Edition.
With this command you may apply an existing Docker enterprise license, or
interactively download one from Docker. In the interactive exchange, you can
sign up for a new trial, or download an existing license. If you are
currently running a Community Edition engine, the daemon will be updated to
the Enterprise Edition Docker engine with additional capabilities and long
term support.
For more information about different Docker Enterprise license types visit
https://www.docker.com/licenses
For non-interactive scriptable deployments, download your license from
https://hub.docker.com/ then specify the file with the '--license' flag.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return runActivate(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.licenseFile, "license", "", "License File")
flags.StringVar(&options.version, "version", "", "Specify engine version (default is to use currently running version)")
flags.StringVar(&options.registryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled")
flags.StringVar(&options.image, "engine-image", containerizedengine.EnterpriseEngineImage, "Specify engine image")
flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template")
flags.BoolVar(&options.displayOnly, "display-only", false, "only display the available licenses and exit")
flags.BoolVar(&options.quiet, "quiet", false, "Only display available licenses by ID")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runActivate(cli command.Cli, options activateOptions) error {
ctx := context.Background()
client, err := cli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
authConfig, err := getRegistryAuth(cli, options.registryPrefix)
if err != nil {
return err
}
var license *model.IssuedLicense
// Lookup on hub if no license provided via params
if options.licenseFile == "" {
if license, err = getLicenses(ctx, authConfig, cli, options); err != nil {
return err
}
if options.displayOnly {
return nil
}
} else {
if license, err = licenseutils.LoadLocalIssuedLicense(ctx, options.licenseFile); err != nil {
return err
}
}
if err = licenseutils.ApplyLicense(ctx, cli.Client(), license); err != nil {
return err
}
opts := containerizedengine.EngineInitOptions{
RegistryPrefix: options.registryPrefix,
EngineImage: options.image,
EngineVersion: options.version,
}
return client.ActivateEngine(ctx, opts, cli.Out(), authConfig,
func(ctx context.Context) error {
client := cli.Client()
_, err := client.Ping(ctx)
return err
})
}
func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) {
user, err := licenseutils.Login(ctx, authConfig)
if err != nil {
return nil, err
}
fmt.Fprintf(cli.Out(), "Looking for existing licenses for %s...\n", user.User.Username)
subs, err := user.GetAvailableLicenses(ctx)
if err != nil {
return nil, err
}
if len(subs) == 0 {
return doTrialFlow(ctx, cli, user)
}
format := options.format
if len(format) == 0 {
format = formatter.TableFormatKey
}
updatesCtx := formatter.Context{
Output: cli.Out(),
Format: formatter.NewSubscriptionsFormat(format, options.quiet),
Trunc: false,
}
if err := formatter.SubscriptionsWrite(updatesCtx, subs); err != nil {
return nil, err
}
if options.displayOnly {
return nil, nil
}
fmt.Fprintf(cli.Out(), "Please pick a license by number: ")
var num int
if _, err := fmt.Fscan(cli.In(), &num); err != nil {
return nil, errors.Wrap(err, "failed to read user input")
}
if num < 0 || num >= len(subs) {
return nil, fmt.Errorf("invalid choice")
}
return user.GetIssuedLicense(ctx, subs[num].ID)
}
func doTrialFlow(ctx context.Context, cli command.Cli, user licenseutils.HubUser) (*model.IssuedLicense, error) {
if !command.PromptForConfirmation(cli.In(), cli.Out(),
"No existing licenses found, would you like to set up a new Enterprise Basic Trial license?") {
return nil, fmt.Errorf("you must have an existing enterprise license or generate a new trial to use the Enterprise Docker Engine")
}
targetID := user.User.ID
// If the user is a member of any organizations, allow trials generated against them
if len(user.Orgs) > 0 {
fmt.Fprintf(cli.Out(), "%d\t%s\n", 0, user.User.Username)
for i, org := range user.Orgs {
fmt.Fprintf(cli.Out(), "%d\t%s\n", i+1, org.Orgname)
}
fmt.Fprintf(cli.Out(), "Please choose an account to generate the trial in:")
var num int
if _, err := fmt.Fscan(cli.In(), &num); err != nil {
return nil, errors.Wrap(err, "failed to read user input")
}
if num < 0 || num > len(user.Orgs) {
return nil, fmt.Errorf("invalid choice")
}
if num > 0 {
targetID = user.Orgs[num-1].ID
}
}
return user.GenerateTrialLicense(ctx, targetID)
}

View File

@ -0,0 +1,37 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestActivateNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newActivateCommand(testCli)
cmd.Flags().Set("license", "invalidpath")
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestActivateBadLicense(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newActivateCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
cmd.Flags().Set("license", "invalidpath")
err := cmd.Execute()
assert.Error(t, err, "open invalidpath: no such file or directory")
}

View File

@ -0,0 +1,33 @@
package engine
import (
"context"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/pkg/errors"
)
func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) {
if registryPrefix == "" {
registryPrefix = "docker.io/docker"
}
distributionRef, err := reference.ParseNormalizedNamed(registryPrefix)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse image name: %s", registryPrefix)
}
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(context.Background(), nil, authResolver(cli), distributionRef.String())
if err != nil {
return nil, errors.Wrap(err, "failed to get imgRefAndAuth")
}
return imgRefAndAuth.AuthConfig(), nil
}
func authResolver(cli command.Cli) func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return func(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig {
return command.ResolveAuthConfig(ctx, cli, index)
}
}

133
cli/command/engine/check.go Normal file
View File

@ -0,0 +1,133 @@
package engine
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/containerizedengine"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
const (
releaseNotePrefix = "https://docs.docker.com/releasenotes"
)
type checkOptions struct {
registryPrefix string
preReleases bool
downgrades bool
upgrades bool
format string
quiet bool
sockPath string
}
func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
var options checkOptions
cmd := &cobra.Command{
Use: "check [OPTIONS]",
Short: "Check for available engine updates",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runCheck(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.registryPrefix, "registry-prefix", "", "Override the existing location where engine images are pulled")
flags.BoolVar(&options.downgrades, "downgrades", false, "Report downgrades (default omits older versions)")
flags.BoolVar(&options.preReleases, "pre-releases", false, "Include pre-release versions")
flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades")
flags.StringVar(&options.format, "format", "", "Pretty-print updates using a Go template")
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display available versions")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runCheck(dockerCli command.Cli, options checkOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
currentOpts, err := client.GetCurrentEngineVersion(ctx)
if err != nil {
return err
}
// override with user provided prefix if specified
if options.registryPrefix != "" {
currentOpts.RegistryPrefix = options.registryPrefix
}
imageName := currentOpts.RegistryPrefix + "/" + currentOpts.EngineImage
currentVersion := currentOpts.EngineVersion
versions, err := client.GetEngineVersions(ctx, dockerCli.RegistryClient(false), currentVersion, imageName)
if err != nil {
return err
}
availUpdates := []containerizedengine.Update{
{Type: "current", Version: currentVersion},
}
if len(versions.Patches) > 0 {
availUpdates = append(availUpdates,
processVersions(
currentVersion,
"patch",
options.preReleases,
versions.Patches)...)
}
if options.upgrades {
availUpdates = append(availUpdates,
processVersions(
currentVersion,
"upgrade",
options.preReleases,
versions.Upgrades)...)
}
if options.downgrades {
availUpdates = append(availUpdates,
processVersions(
currentVersion,
"downgrade",
options.preReleases,
versions.Downgrades)...)
}
format := options.format
if len(format) == 0 {
format = formatter.TableFormatKey
}
updatesCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewUpdatesFormat(format, options.quiet),
Trunc: false,
}
return formatter.UpdatesWrite(updatesCtx, availUpdates)
}
func processVersions(currentVersion, verType string,
includePrerelease bool,
versions []containerizedengine.DockerVersion) []containerizedengine.Update {
availUpdates := []containerizedengine.Update{}
for _, ver := range versions {
if !includePrerelease && ver.Prerelease() != "" {
continue
}
if ver.Tag != currentVersion {
availUpdates = append(availUpdates, containerizedengine.Update{
Type: verType,
Version: ver.Tag,
Notes: fmt.Sprintf("%s/%s", releaseNotePrefix, ver.Tag),
})
}
}
return availUpdates
}

View File

@ -0,0 +1,143 @@
package engine
import (
"context"
"fmt"
"testing"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/client"
ver "github.com/hashicorp/go-version"
"gotest.tools/assert"
"gotest.tools/golden"
)
var (
testCli = test.NewFakeCli(&client.Client{})
)
func TestCheckForUpdatesNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestCheckForUpdatesNoCurrentVersion(t *testing.T) {
retErr := fmt.Errorf("some failure")
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
return containerizedengine.EngineInitOptions{}, retErr
}
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.Assert(t, err == retErr)
}
func TestCheckForUpdatesGetEngineVersionsFail(t *testing.T) {
retErr := fmt.Errorf("some failure")
getEngineVersionsFunc := func(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
return containerizedengine.AvailableVersions{}, retErr
}
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getEngineVersionsFunc: getEngineVersionsFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.Assert(t, err == retErr)
}
func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
return containerizedengine.EngineInitOptions{
EngineImage: "current engine",
EngineVersion: "1.1.0",
}, nil
}
getEngineVersionsFunc := func(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
return containerizedengine.AvailableVersions{
Downgrades: parseVersions(t, "1.0.1", "1.0.2", "1.0.3-beta1"),
Patches: parseVersions(t, "1.1.1", "1.1.2", "1.1.3-beta1"),
Upgrades: parseVersions(t, "1.2.0", "2.0.0", "2.1.0-beta1"),
}, nil
}
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getEngineVersionsFunc: getEngineVersionsFunc,
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.Flags().Set("pre-releases", "true")
cmd.Flags().Set("downgrades", "true")
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, testCli.OutBuffer().String(), "check-all.golden")
testCli.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "true")
err = cmd.Execute()
assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-no-prerelease.golden")
testCli.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "false")
err = cmd.Execute()
assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-no-downgrades.golden")
testCli.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "false")
cmd.Flags().Set("upgrades", "false")
err = cmd.Execute()
assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-patches-only.golden")
}
func makeVersion(t *testing.T, tag string) containerizedengine.DockerVersion {
v, err := ver.NewVersion(tag)
assert.NilError(t, err)
return containerizedengine.DockerVersion{Version: *v, Tag: tag}
}
func parseVersions(t *testing.T, tags ...string) []containerizedengine.DockerVersion {
ret := make([]containerizedengine.DockerVersion, len(tags))
for i, tag := range tags {
ret[i] = makeVersion(t, tag)
}
return ret
}

View File

@ -0,0 +1,105 @@
package engine
import (
"context"
"github.com/containerd/containerd"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/docker/api/types"
)
type (
fakeContainerizedEngineClient struct {
closeFunc func() error
activateEngineFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
initEngineFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
doUpdateFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
getEngineVersionsFunc func(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion,
imageName string) (containerizedengine.AvailableVersions, error)
getEngineFunc func(ctx context.Context) (containerd.Container, error)
removeEngineFunc func(ctx context.Context, engine containerd.Container) error
getCurrentEngineVersionFunc func(ctx context.Context) (containerizedengine.EngineInitOptions, error)
}
)
func (w *fakeContainerizedEngineClient) Close() error {
if w.closeFunc != nil {
return w.closeFunc()
}
return nil
}
func (w *fakeContainerizedEngineClient) ActivateEngine(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error {
if w.activateEngineFunc != nil {
return w.activateEngineFunc(ctx, opts, out, authConfig, healthfn)
}
return nil
}
func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error {
if w.initEngineFunc != nil {
return w.initEngineFunc(ctx, opts, out, authConfig, healthfn)
}
return nil
}
func (w *fakeContainerizedEngineClient) DoUpdate(ctx context.Context,
opts containerizedengine.EngineInitOptions,
out containerizedengine.OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error {
if w.doUpdateFunc != nil {
return w.doUpdateFunc(ctx, opts, out, authConfig, healthfn)
}
return nil
}
func (w *fakeContainerizedEngineClient) GetEngineVersions(ctx context.Context,
registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
if w.getEngineVersionsFunc != nil {
return w.getEngineVersionsFunc(ctx, registryClient, currentVersion, imageName)
}
return containerizedengine.AvailableVersions{}, nil
}
func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containerd.Container, error) {
if w.getEngineFunc != nil {
return w.getEngineFunc(ctx)
}
return nil, nil
}
func (w *fakeContainerizedEngineClient) RemoveEngine(ctx context.Context, engine containerd.Container) error {
if w.removeEngineFunc != nil {
return w.removeEngineFunc(ctx, engine)
}
return nil
}
func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
if w.getCurrentEngineVersionFunc != nil {
return w.getCurrentEngineVersionFunc(ctx)
}
return containerizedengine.EngineInitOptions{}, nil
}

25
cli/command/engine/cmd.go Normal file
View File

@ -0,0 +1,25 @@
package engine
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// NewEngineCommand returns a cobra command for `engine` subcommands
func NewEngineCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "engine COMMAND",
Short: "Manage the docker engine",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newInitCommand(dockerCli),
newActivateCommand(dockerCli),
newCheckForUpdatesCommand(dockerCli),
newUpdateCommand(dockerCli),
newRmCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,14 @@
package engine
import (
"testing"
"gotest.tools/assert"
)
func TestNewEngineCommand(t *testing.T) {
cmd := NewEngineCommand(testCli)
subcommands := cmd.Commands()
assert.Assert(t, len(subcommands) == 5)
}

View File

@ -0,0 +1,62 @@
package engine
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/containerizedengine"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type extendedEngineInitOptions struct {
containerizedengine.EngineInitOptions
sockPath string
}
func newInitCommand(dockerCli command.Cli) *cobra.Command {
var options extendedEngineInitOptions
cmd := &cobra.Command{
Use: "init [OPTIONS]",
Short: "Initialize a local engine",
Long: `This command will initialize a local engine running on containerd.
Configuration of the engine is managed through the daemon.json configuration
file on the host and may be pre-created before running the 'init' command.
`,
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runInit(dockerCli, options)
},
Annotations: map[string]string{"experimentalCLI": ""},
}
flags := cmd.Flags()
flags.StringVar(&options.EngineVersion, "version", cli.Version, "Specify engine version")
flags.StringVar(&options.EngineImage, "engine-image", containerizedengine.CommunityEngineImage, "Specify engine image")
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled")
flags.StringVar(&options.ConfigFile, "config-file", "/etc/docker/daemon.json", "Specify the location of the daemon configuration file on the host")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runInit(dockerCli command.Cli, options extendedEngineInitOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
if err != nil {
return err
}
return client.InitEngine(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig,
func(ctx context.Context) error {
client := dockerCli.Client()
_, err := client.Ping(ctx)
return err
})
}

View File

@ -0,0 +1,33 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestInitNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newInitCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestInitHappy(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newInitCommand(testCli)
err := cmd.Execute()
assert.NilError(t, err)
}

54
cli/command/engine/rm.go Normal file
View File

@ -0,0 +1,54 @@
package engine
import (
"context"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// TODO - consider adding a "purge" flag that also removes
// configuration files and the docker root dir.
type rmOptions struct {
sockPath string
}
func newRmCommand(dockerCli command.Cli) *cobra.Command {
var options rmOptions
cmd := &cobra.Command{
Use: "rm [OPTIONS]",
Short: "Remove the local engine",
Long: `This command will remove the local engine running on containerd.
No state files will be removed from the host filesystem.
`,
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runRm(dockerCli, options)
},
Annotations: map[string]string{"experimentalCLI": ""},
}
flags := cmd.Flags()
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runRm(dockerCli command.Cli, options rmOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
engine, err := client.GetEngine(ctx)
if err != nil {
return err
}
return client.RemoveEngine(ctx, engine)
}

View File

@ -0,0 +1,33 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestRmNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newRmCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestRmHappy(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newRmCommand(testCli)
err := cmd.Execute()
assert.NilError(t, err)
}

View File

@ -0,0 +1,11 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
patch 1.1.3-beta1 https://docs.docker.com/releasenotes/1.1.3-beta1
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0
upgrade 2.1.0-beta1 https://docs.docker.com/releasenotes/2.1.0-beta1
downgrade 1.0.1 https://docs.docker.com/releasenotes/1.0.1
downgrade 1.0.2 https://docs.docker.com/releasenotes/1.0.2
downgrade 1.0.3-beta1 https://docs.docker.com/releasenotes/1.0.3-beta1

View File

@ -0,0 +1,6 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0

View File

@ -0,0 +1,8 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0
downgrade 1.0.1 https://docs.docker.com/releasenotes/1.0.1
downgrade 1.0.2 https://docs.docker.com/releasenotes/1.0.2

View File

@ -0,0 +1,4 @@
TYPE VERSION NOTES
current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2

View File

@ -0,0 +1,68 @@
package engine
import (
"context"
"fmt"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
var options extendedEngineInitOptions
cmd := &cobra.Command{
Use: "update [OPTIONS]",
Short: "Update a local engine",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdate(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version")
flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image")
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "", "Override the current location where engine images are pulled")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd
}
func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error {
ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil {
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
if options.EngineImage == "" || options.RegistryPrefix == "" {
currentOpts, err := client.GetCurrentEngineVersion(ctx)
if err != nil {
return err
}
if options.EngineImage == "" {
options.EngineImage = currentOpts.EngineImage
}
if options.RegistryPrefix == "" {
options.RegistryPrefix = currentOpts.RegistryPrefix
}
}
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
if err != nil {
return err
}
if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig,
func(ctx context.Context) error {
client := dockerCli.Client()
_, err := client.Ping(ctx)
return err
}); err != nil {
return err
}
fmt.Fprintln(dockerCli.Out(), "Success! The docker engine is now running.")
return nil
}

View File

@ -0,0 +1,35 @@
package engine
import (
"fmt"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
)
func TestUpdateNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return nil, fmt.Errorf("some error")
},
)
cmd := newUpdateCommand(testCli)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
assert.ErrorContains(t, err, "unable to access local containerd")
}
func TestUpdateHappy(t *testing.T) {
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newUpdateCommand(testCli)
cmd.Flags().Set("registry-prefix", "docker.io/docker")
cmd.Flags().Set("version", "someversion")
err := cmd.Execute()
assert.NilError(t, err)
}

View File

@ -269,7 +269,10 @@ func DisplayablePorts(ports []types.Port) string {
var result []string
var hostMappings []string
var groupMapKeys []string
sort.Sort(byPortInfo(ports))
sort.Slice(ports, func(i, j int) bool {
return comparePorts(ports[i], ports[j])
})
for _, port := range ports {
current := port.PrivatePort
portKey := port.Type
@ -322,23 +325,18 @@ func formGroup(key string, start, last uint16) string {
return fmt.Sprintf("%s/%s", group, groupType)
}
// byPortInfo is a temporary type used to sort types.Port by its fields
type byPortInfo []types.Port
func (r byPortInfo) Len() int { return len(r) }
func (r byPortInfo) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byPortInfo) Less(i, j int) bool {
if r[i].PrivatePort != r[j].PrivatePort {
return r[i].PrivatePort < r[j].PrivatePort
func comparePorts(i, j types.Port) bool {
if i.PrivatePort != j.PrivatePort {
return i.PrivatePort < j.PrivatePort
}
if r[i].IP != r[j].IP {
return r[i].IP < r[j].IP
if i.IP != j.IP {
return i.IP < j.IP
}
if r[i].PublicPort != r[j].PublicPort {
return r[i].PublicPort < r[j].PublicPort
if i.PublicPort != j.PublicPort {
return i.PublicPort < j.PublicPort
}
return r[i].Type < r[j].Type
return i.Type < j.Type
}

View File

@ -0,0 +1,154 @@
package formatter
import (
"time"
"github.com/docker/cli/internal/licenseutils"
"github.com/docker/licensing/model"
)
const (
defaultSubscriptionsTableFormat = "table {{.Num}}\t{{.Owner}}\t{{.ProductID}}\t{{.Expires}}\t{{.ComponentsString}}"
defaultSubscriptionsQuietFormat = "{{.Num}}:{{.Summary}}"
numHeader = "NUM"
ownerHeader = "OWNER"
licenseNameHeader = "NAME"
idHeader = "ID"
dockerIDHeader = "DOCKER ID"
productIDHeader = "PRODUCT ID"
productRatePlanHeader = "PRODUCT RATE PLAN"
productRatePlanIDHeader = "PRODUCT RATE PLAN ID"
startHeader = "START"
expiresHeader = "EXPIRES"
stateHeader = "STATE"
eusaHeader = "EUSA"
pricingComponentsHeader = "PRICING COMPONENTS"
)
// NewSubscriptionsFormat returns a Format for rendering using a license Context
func NewSubscriptionsFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultSubscriptionsQuietFormat
}
return defaultSubscriptionsTableFormat
case RawFormatKey:
if quiet {
return `license: {{.ID}}`
}
return `license: {{.ID}}\nname: {{.Name}}\nowner: {{.Owner}}\ncomponents: {{.ComponentsString}}\n`
}
return Format(source)
}
// SubscriptionsWrite writes the context
func SubscriptionsWrite(ctx Context, subs []licenseutils.LicenseDisplay) error {
render := func(format func(subContext subContext) error) error {
for _, sub := range subs {
licenseCtx := &licenseContext{trunc: ctx.Trunc, l: sub}
if err := format(licenseCtx); err != nil {
return err
}
}
return nil
}
licenseCtx := licenseContext{}
licenseCtx.header = map[string]string{
"Num": numHeader,
"Owner": ownerHeader,
"Name": licenseNameHeader,
"ID": idHeader,
"DockerID": dockerIDHeader,
"ProductID": productIDHeader,
"ProductRatePlan": productRatePlanHeader,
"ProductRatePlanID": productRatePlanIDHeader,
"Start": startHeader,
"Expires": expiresHeader,
"State": stateHeader,
"Eusa": eusaHeader,
"ComponentsString": pricingComponentsHeader,
}
return ctx.Write(&licenseCtx, render)
}
type licenseContext struct {
HeaderContext
trunc bool
l licenseutils.LicenseDisplay
}
func (c *licenseContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *licenseContext) Num() int {
return c.l.Num
}
func (c *licenseContext) Owner() string {
return c.l.Owner
}
func (c *licenseContext) ComponentsString() string {
return c.l.ComponentsString
}
func (c *licenseContext) Summary() string {
return c.l.String()
}
func (c *licenseContext) Name() string {
return c.l.Name
}
func (c *licenseContext) ID() string {
return c.l.ID
}
func (c *licenseContext) DockerID() string {
return c.l.DockerID
}
func (c *licenseContext) ProductID() string {
return c.l.ProductID
}
func (c *licenseContext) ProductRatePlan() string {
return c.l.ProductRatePlan
}
func (c *licenseContext) ProductRatePlanID() string {
return c.l.ProductRatePlanID
}
func (c *licenseContext) Start() *time.Time {
return c.l.Start
}
func (c *licenseContext) Expires() *time.Time {
return c.l.Expires
}
func (c *licenseContext) State() string {
return c.l.State
}
func (c *licenseContext) Eusa() *model.EusaState {
return c.l.Eusa
}
func (c *licenseContext) PricingComponents() []model.SubscriptionPricingComponent {
// Dereference the pricing component pointers in the pricing components
// so it can be rendered properly with the template formatter
var ret []model.SubscriptionPricingComponent
for _, spc := range c.l.PricingComponents {
if spc == nil {
continue
}
ret = append(ret, *spc)
}
return ret
}

View File

@ -0,0 +1,256 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"time"
"github.com/docker/cli/internal/licenseutils"
"github.com/docker/licensing/model"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestSubscriptionContextWrite(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: NewSubscriptionsFormat("table", false)},
`NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS
1 owner1 productid1 2020-01-01 10:00:00 +0000 UTC compstring
2 owner2 productid2 2020-01-01 10:00:00 +0000 UTC compstring
`,
},
{
Context{Format: NewSubscriptionsFormat("table", true)},
`1:License Name: name1 Quantity: 10 nodes Expiration date: 2020-01-01
2:License Name: name2 Quantity: 20 nodes Expiration date: 2020-01-01
`,
},
{
Context{Format: NewSubscriptionsFormat("table {{.Owner}}", false)},
`OWNER
owner1
owner2
`,
},
{
Context{Format: NewSubscriptionsFormat("table {{.Owner}}", true)},
`OWNER
owner1
owner2
`,
},
// Raw Format
{
Context{Format: NewSubscriptionsFormat("raw", false)},
`license: id1
name: name1
owner: owner1
components: compstring
license: id2
name: name2
owner: owner2
components: compstring
`,
},
{
Context{Format: NewSubscriptionsFormat("raw", true)},
`license: id1
license: id2
`,
},
// Custom Format
{
Context{Format: NewSubscriptionsFormat("{{.Owner}}", false)},
`owner1
owner2
`,
},
}
expiration, _ := time.Parse(time.RFC822, "01 Jan 20 10:00 UTC")
for _, testcase := range cases {
subscriptions := []licenseutils.LicenseDisplay{
{
Num: 1,
Owner: "owner1",
Subscription: model.Subscription{
ID: "id1",
Name: "name1",
ProductID: "productid1",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 10,
},
},
},
ComponentsString: "compstring",
},
{
Num: 2,
Owner: "owner2",
Subscription: model.Subscription{
ID: "id2",
Name: "name2",
ProductID: "productid2",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 20,
},
},
},
ComponentsString: "compstring",
},
}
out := &bytes.Buffer{}
testcase.context.Output = out
err := SubscriptionsWrite(testcase.context, subscriptions)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Check(t, is.Equal(testcase.expected, out.String()))
}
}
}
func TestSubscriptionContextWriteJSON(t *testing.T) {
expiration, _ := time.Parse(time.RFC822, "01 Jan 20 10:00 UTC")
subscriptions := []licenseutils.LicenseDisplay{
{
Num: 1,
Owner: "owner1",
Subscription: model.Subscription{
ID: "id1",
Name: "name1",
ProductID: "productid1",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 10,
},
},
},
ComponentsString: "compstring",
},
{
Num: 2,
Owner: "owner2",
Subscription: model.Subscription{
ID: "id2",
Name: "name2",
ProductID: "productid2",
Expires: &expiration,
PricingComponents: model.PricingComponents{
&model.SubscriptionPricingComponent{
Name: "nodes",
Value: 20,
},
},
},
ComponentsString: "compstring",
},
}
expectedJSONs := []map[string]interface{}{
{
"Owner": "owner1",
"ComponentsString": "compstring",
"Expires": "2020-01-01T10:00:00Z",
"DockerID": "",
"Eusa": nil,
"ID": "id1",
"Start": nil,
"Name": "name1",
"Num": float64(1),
"PricingComponents": []interface{}{
map[string]interface{}{
"name": "nodes",
"value": float64(10),
},
},
"ProductID": "productid1",
"ProductRatePlan": "",
"ProductRatePlanID": "",
"State": "",
"Summary": "License Name: name1\tQuantity: 10 nodes\tExpiration date: 2020-01-01",
},
{
"Owner": "owner2",
"ComponentsString": "compstring",
"Expires": "2020-01-01T10:00:00Z",
"DockerID": "",
"Eusa": nil,
"ID": "id2",
"Start": nil,
"Name": "name2",
"Num": float64(2),
"PricingComponents": []interface{}{
map[string]interface{}{
"name": "nodes",
"value": float64(20),
},
},
"ProductID": "productid2",
"ProductRatePlan": "",
"ProductRatePlanID": "",
"State": "",
"Summary": "License Name: name2\tQuantity: 20 nodes\tExpiration date: 2020-01-01",
},
}
out := &bytes.Buffer{}
err := SubscriptionsWrite(Context{Format: "{{json .}}", Output: out}, subscriptions)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Check(t, is.DeepEqual(expectedJSONs[i], m))
}
}
func TestSubscriptionContextWriteJSONField(t *testing.T) {
subscriptions := []licenseutils.LicenseDisplay{
{Num: 1, Owner: "owner1"},
{Num: 2, Owner: "owner2"},
}
out := &bytes.Buffer{}
err := SubscriptionsWrite(Context{Format: "{{json .Owner}}", Output: out}, subscriptions)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Check(t, is.Equal(subscriptions[i].Owner, s))
}
}

View File

@ -599,7 +599,13 @@ func (c *serviceContext) Ports() string {
pr := portRange{}
ports := []string{}
sort.Sort(byProtocolAndPublishedPort(c.service.Endpoint.Ports))
servicePorts := c.service.Endpoint.Ports
sort.Slice(servicePorts, func(i, j int) bool {
if servicePorts[i].Protocol == servicePorts[j].Protocol {
return servicePorts[i].PublishedPort < servicePorts[j].PublishedPort
}
return servicePorts[i].Protocol < servicePorts[j].Protocol
})
for _, p := range c.service.Endpoint.Ports {
if p.PublishMode == swarm.PortConfigPublishModeIngress {
@ -633,14 +639,3 @@ func (c *serviceContext) Ports() string {
}
return strings.Join(ports, ", ")
}
type byProtocolAndPublishedPort []swarm.PortConfig
func (a byProtocolAndPublishedPort) Len() int { return len(a) }
func (a byProtocolAndPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byProtocolAndPublishedPort) Less(i, j int) bool {
if a[i].Protocol == a[j].Protocol {
return a[i].PublishedPort < a[j].PublishedPort
}
return a[i].Protocol < a[j].Protocol
}

View File

@ -133,18 +133,3 @@ func (c *signerInfoContext) Keys() string {
func (c *signerInfoContext) Signer() string {
return c.s.Name
}
// SignerInfoList helps sort []SignerInfo by signer names
type SignerInfoList []SignerInfo
func (signerInfoComp SignerInfoList) Len() int {
return len(signerInfoComp)
}
func (signerInfoComp SignerInfoList) Less(i, j int) bool {
return signerInfoComp[i].Name < signerInfoComp[j].Name
}
func (signerInfoComp SignerInfoList) Swap(i, j int) {
signerInfoComp[i], signerInfoComp[j] = signerInfoComp[j], signerInfoComp[i]
}

View File

@ -222,7 +222,7 @@ eve foobarbazquxquux, key31, key32
}
for _, testcase := range cases {
signerInfo := SignerInfoList{
signerInfo := []SignerInfo{
{Name: "alice", Keys: []string{"key11", "key12"}},
{Name: "bob", Keys: []string{"key21"}},
{Name: "eve", Keys: []string{"key31", "key32", "foobarbazquxquux"}},

View File

@ -0,0 +1,73 @@
package formatter
import (
"github.com/docker/cli/internal/containerizedengine"
)
const (
defaultUpdatesTableFormat = "table {{.Type}}\t{{.Version}}\t{{.Notes}}"
defaultUpdatesQuietFormat = "{{.Version}}"
updatesTypeHeader = "TYPE"
versionHeader = "VERSION"
notesHeader = "NOTES"
)
// NewUpdatesFormat returns a Format for rendering using a updates context
func NewUpdatesFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultUpdatesQuietFormat
}
return defaultUpdatesTableFormat
case RawFormatKey:
if quiet {
return `update_version: {{.Version}}`
}
return `update_version: {{.Version}}\ntype: {{.Type}}\nnotes: {{.Notes}}\n`
}
return Format(source)
}
// UpdatesWrite writes the context
func UpdatesWrite(ctx Context, availableUpdates []containerizedengine.Update) error {
render := func(format func(subContext subContext) error) error {
for _, update := range availableUpdates {
updatesCtx := &updateContext{trunc: ctx.Trunc, u: update}
if err := format(updatesCtx); err != nil {
return err
}
}
return nil
}
updatesCtx := updateContext{}
updatesCtx.header = map[string]string{
"Type": updatesTypeHeader,
"Version": versionHeader,
"Notes": notesHeader,
}
return ctx.Write(&updatesCtx, render)
}
type updateContext struct {
HeaderContext
trunc bool
u containerizedengine.Update
}
func (c *updateContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *updateContext) Type() string {
return c.u.Type
}
func (c *updateContext) Version() string {
return c.u.Version
}
func (c *updateContext) Notes() string {
return c.u.Notes
}

View File

@ -0,0 +1,143 @@
package formatter
import (
"bytes"
"encoding/json"
"strings"
"testing"
"github.com/docker/cli/internal/containerizedengine"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestUpdateContextWrite(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: NewUpdatesFormat("table", false)},
`TYPE VERSION NOTES
updateType1 version1 description 1
updateType2 version2 description 2
`,
},
{
Context{Format: NewUpdatesFormat("table", true)},
`version1
version2
`,
},
{
Context{Format: NewUpdatesFormat("table {{.Version}}", false)},
`VERSION
version1
version2
`,
},
{
Context{Format: NewUpdatesFormat("table {{.Version}}", true)},
`VERSION
version1
version2
`,
},
// Raw Format
{
Context{Format: NewUpdatesFormat("raw", false)},
`update_version: version1
type: updateType1
notes: description 1
update_version: version2
type: updateType2
notes: description 2
`,
},
{
Context{Format: NewUpdatesFormat("raw", true)},
`update_version: version1
update_version: version2
`,
},
// Custom Format
{
Context{Format: NewUpdatesFormat("{{.Version}}", false)},
`version1
version2
`,
},
}
for _, testcase := range cases {
updates := []containerizedengine.Update{
{Type: "updateType1", Version: "version1", Notes: "description 1"},
{Type: "updateType2", Version: "version2", Notes: "description 2"},
}
out := &bytes.Buffer{}
testcase.context.Output = out
err := UpdatesWrite(testcase.context, updates)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
assert.Check(t, is.Equal(testcase.expected, out.String()))
}
}
}
func TestUpdateContextWriteJSON(t *testing.T) {
updates := []containerizedengine.Update{
{Type: "updateType1", Version: "version1", Notes: "note1"},
{Type: "updateType2", Version: "version2", Notes: "note2"},
}
expectedJSONs := []map[string]interface{}{
{"Version": "version1", "Notes": "note1", "Type": "updateType1"},
{"Version": "version2", "Notes": "note2", "Type": "updateType2"},
}
out := &bytes.Buffer{}
err := UpdatesWrite(Context{Format: "{{json .}}", Output: out}, updates)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var m map[string]interface{}
if err := json.Unmarshal([]byte(line), &m); err != nil {
t.Fatal(err)
}
assert.Check(t, is.DeepEqual(expectedJSONs[i], m))
}
}
func TestUpdateContextWriteJSONField(t *testing.T) {
updates := []containerizedengine.Update{
{Type: "updateType1", Version: "version1"},
{Type: "updateType2", Version: "version2"},
}
out := &bytes.Buffer{}
err := UpdatesWrite(Context{Format: "{{json .Type}}", Output: out}, updates)
if err != nil {
t.Fatal(err)
}
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
var s string
if err := json.Unmarshal([]byte(line), &s); err != nil {
t.Fatal(err)
}
assert.Check(t, is.Equal(updates[i].Type, s))
}
}

View File

@ -58,7 +58,7 @@ type buildOptions struct {
isolation string
quiet bool
noCache bool
console opts.NullableBool
progress string
rm bool
forceRm bool
pull bool
@ -72,6 +72,7 @@ type buildOptions struct {
stream bool
platform string
untrusted bool
secrets []string
}
// dockerfileFromStdin returns true when the user specified that the Dockerfile
@ -153,9 +154,10 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.SetAnnotation("stream", "experimental", nil)
flags.SetAnnotation("stream", "version", []string{"1.31"})
flags.Var(&options.console, "console", "Show console output (with buildkit only) (true, false, auto)")
flags.SetAnnotation("console", "experimental", nil)
flags.SetAnnotation("console", "version", []string{"1.38"})
flags.StringVar(&options.progress, "progress", "auto", "Set type of progress output (only if BuildKit enabled) (auto, plain, tty). Use plain to show container output")
flags.StringArrayVar(&options.secrets, "secret", []string{}, "Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret")
flags.SetAnnotation("secret", "version", []string{"1.39"})
return cmd
}
@ -185,6 +187,8 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
if enableBuildkit {
return runBuildBuildKit(dockerCli, options)
}
} else if dockerCli.ServerInfo().BuildkitVersion == types.BuilderBuildKit {
return runBuildBuildKit(dockerCli, options)
}
var (
@ -270,15 +274,12 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
}
// And canonicalize dockerfile name to a platform-independent one
relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile)
if err != nil {
return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err)
}
relDockerfile = archive.CanonicalTarNameForPath(relDockerfile)
excludes = build.TrimBuildFilesFromExcludes(excludes, relDockerfile, options.dockerfileFromStdin())
buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{
ExcludePatterns: excludes,
ChownOpts: &idtools.IDPair{UID: 0, GID: 0},
ChownOpts: &idtools.Identity{UID: 0, GID: 0},
})
if err != nil {
return err

View File

@ -3,6 +3,7 @@ package image
import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io"
@ -15,14 +16,17 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image/build"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/pkg/urlutil"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/pkg/errors"
@ -127,6 +131,13 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
}
s.Allow(authprovider.NewDockerAuthProvider())
if len(options.secrets) > 0 {
sp, err := parseSecretSpecs(options.secrets)
if err != nil {
return errors.Wrapf(err, "could not parse secrets: %v", options.secrets)
}
s.Allow(sp)
}
eg, ctx := errgroup.WithContext(ctx)
@ -191,16 +202,19 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, opt
t := newTracer()
ssArr := []*client.SolveStatus{}
if err := opts.ValidateProgressOutput(options.progress); err != nil {
return err
}
displayStatus := func(out *os.File, displayCh chan *client.SolveStatus) {
var c console.Console
// TODO: Handle interactive output in non-interactive environment.
consoleOpt := options.console.Value()
if cons, err := console.ConsoleFromFile(out); err == nil && (consoleOpt == nil || *consoleOpt) {
// TODO: Handle tty output in non-tty environment.
if cons, err := console.ConsoleFromFile(out); err == nil && (options.progress == "auto" || options.progress == "tty") {
c = cons
}
// not using shared context to not disrupt display but let is finish reporting errors
eg.Go(func() error {
return progressui.DisplaySolveStatus(context.TODO(), c, out, displayCh)
return progressui.DisplaySolveStatus(context.TODO(), "", c, out, displayCh)
})
}
@ -344,3 +358,53 @@ func (t *tracer) write(msg jsonmessage.JSONMessage) {
t.displayCh <- &s
}
func parseSecretSpecs(sl []string) (session.Attachable, error) {
fs := make([]secretsprovider.FileSource, 0, len(sl))
for _, v := range sl {
s, err := parseSecret(v)
if err != nil {
return nil, err
}
fs = append(fs, *s)
}
store, err := secretsprovider.NewFileStore(fs)
if err != nil {
return nil, err
}
return secretsprovider.NewSecretProvider(store), nil
}
func parseSecret(value string) (*secretsprovider.FileSource, error) {
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return nil, errors.Wrap(err, "failed to parse csv secret")
}
fs := secretsprovider.FileSource{}
for _, field := range fields {
parts := strings.SplitN(field, "=", 2)
key := strings.ToLower(parts[0])
if len(parts) != 2 {
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field)
}
value := parts[1]
switch key {
case "type":
if value != "file" {
return nil, errors.Errorf("unsupported secret type %q", value)
}
case "id":
fs.ID = value
case "source", "src":
fs.FilePath = value
default:
return nil, errors.Errorf("unexpected key '%s' in '%s'", key, field)
}
}
return &fs, nil
}

View File

@ -28,6 +28,9 @@ import (
const clientSessionRemote = "client-session"
func isSessionSupported(dockerCli command.Cli) bool {
if versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {
return true
}
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
}

View File

@ -5,6 +5,7 @@ import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"io/ioutil"
"os"
@ -17,6 +18,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/google/go-cmp/cmp"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"gotest.tools/assert"
"gotest.tools/fs"
"gotest.tools/skip"
@ -173,6 +175,66 @@ RUN echo hello world
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
}
func TestParseSecret(t *testing.T) {
type testcase struct {
value string
errExpected bool
errMatch string
filesource *secretsprovider.FileSource
}
var testcases = []testcase{
{
value: "",
errExpected: true,
}, {
value: "foobar",
errExpected: true,
errMatch: "must be a key=value pair",
}, {
value: "foo,bar",
errExpected: true,
errMatch: "must be a key=value pair",
}, {
value: "foo=bar",
errExpected: true,
errMatch: "unexpected key",
}, {
value: "src=somefile",
filesource: &secretsprovider.FileSource{FilePath: "somefile"},
}, {
value: "source=somefile",
filesource: &secretsprovider.FileSource{FilePath: "somefile"},
}, {
value: "id=mysecret",
filesource: &secretsprovider.FileSource{ID: "mysecret"},
}, {
value: "id=mysecret,src=somefile",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "somefile"},
}, {
value: "id=mysecret,source=somefile,type=file",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "somefile"},
}, {
value: "id=mysecret,src=somefile,src=othersecretfile",
filesource: &secretsprovider.FileSource{ID: "mysecret", FilePath: "othersecretfile"},
}, {
value: "type=invalid",
errExpected: true,
errMatch: "unsupported secret type",
},
}
for _, tc := range testcases {
t.Run(tc.value, func(t *testing.T) {
secret, err := parseSecret(tc.value)
assert.Equal(t, err != nil, tc.errExpected, fmt.Sprintf("err=%v errExpected=%t", err, tc.errExpected))
if tc.errMatch != "" {
assert.ErrorContains(t, err, tc.errMatch)
}
assert.DeepEqual(t, secret, tc.filesource)
})
}
}
type fakeBuild struct {
context *tar.Reader
options types.ImageBuildOptions

View File

@ -3,7 +3,6 @@ package image
import (
"context"
"fmt"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
@ -74,20 +73,14 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
}
if len(report.ImagesDeleted) > 0 {
var sb strings.Builder
sb.WriteString("Deleted Images:\n")
output = "Deleted Images:\n"
for _, st := range report.ImagesDeleted {
if st.Untagged != "" {
sb.WriteString("untagged: ")
sb.WriteString(st.Untagged)
sb.WriteByte('\n')
output += fmt.Sprintln("untagged:", st.Untagged)
} else {
sb.WriteString("deleted: ")
sb.WriteString(st.Deleted)
sb.WriteByte('\n')
output += fmt.Sprintln("deleted:", st.Deleted)
}
}
output = sb.String()
spaceReclaimed = report.SpaceReclaimed
}

View File

@ -15,6 +15,7 @@ type fakeRegistryClient struct {
getManifestListFunc func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error)
mountBlobFunc func(ctx context.Context, source reference.Canonical, target reference.Named) error
putManifestFunc func(ctx context.Context, source reference.Named, mf distribution.Manifest) (digest.Digest, error)
getTagsFunc func(ctx context.Context, ref reference.Named) ([]string, error)
}
func (c *fakeRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
@ -45,4 +46,11 @@ func (c *fakeRegistryClient) PutManifest(ctx context.Context, ref reference.Name
return digest.Digest(""), nil
}
func (c *fakeRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([]string, error) {
if c.getTagsFunc != nil {
return c.getTagsFunc(ctx, ref)
}
return nil, nil
}
var _ client.RegistryClient = &fakeRegistryClient{}

View File

@ -10,14 +10,9 @@ import (
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type byNetworkName []types.NetworkResource
func (r byNetworkName) Len() int { return len(r) }
func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name }
type listOptions struct {
quiet bool
noTrunc bool
@ -64,7 +59,9 @@ func runList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Sort(byNetworkName(networkResources))
sort.Slice(networkResources, func(i, j int) bool {
return sortorder.NaturalLess(networkResources[i].Name, networkResources[j].Name)
})
networksCtx := formatter.Context{
Output: dockerCli.Out(),

View File

@ -3,7 +3,6 @@ package network
import (
"context"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
@ -41,23 +40,55 @@ func TestNetworkListErrors(t *testing.T) {
}
}
func TestNetworkListWithFlags(t *testing.T) {
expectedOpts := types.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")),
func TestNetworkList(t *testing.T) {
testCases := []struct {
doc string
networkListFunc func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error)
flags map[string]string
golden string
}{
{
doc: "network list with flags",
flags: map[string]string{
"filter": "image.name=ubuntu",
},
golden: "network-list.golden",
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
expectedOpts := types.NetworkListOptions{
Filters: filters.NewArgs(filters.Arg("image.name", "ubuntu")),
}
assert.Check(t, is.DeepEqual(expectedOpts, options, cmp.AllowUnexported(filters.Args{})))
return []types.NetworkResource{*NetworkResource(NetworkResourceID("123454321"),
NetworkResourceName("network_1"),
NetworkResourceDriver("09.7.01"),
NetworkResourceScope("global"))}, nil
},
},
{
doc: "network list sort order",
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "network-list-sort.golden",
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
return []types.NetworkResource{
*NetworkResource(NetworkResourceName("network-2-foo")),
*NetworkResource(NetworkResourceName("network-1-foo")),
*NetworkResource(NetworkResourceName("network-10-foo"))}, nil
},
},
}
cli := test.NewFakeCli(&fakeClient{
networkListFunc: func(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) {
assert.Check(t, is.DeepEqual(expectedOpts, options, cmp.AllowUnexported(filters.Args{})))
return []types.NetworkResource{*NetworkResource(NetworkResourceID("123454321"),
NetworkResourceName("network_1"),
NetworkResourceDriver("09.7.01"),
NetworkResourceScope("global"))}, nil
},
})
cmd := newListCommand(cli)
cmd.Flags().Set("filter", "image.name=ubuntu")
assert.NilError(t, cmd.Execute())
golden.Assert(t, strings.TrimSpace(cli.OutBuffer().String()), "network-list.golden")
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{networkListFunc: tc.networkListFunc})
cmd := newListCommand(cli)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
})
}
}

View File

@ -0,0 +1,3 @@
network-1-foo
network-2-foo
network-10-foo

View File

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

View File

@ -9,19 +9,10 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type byHostname []swarm.Node
func (n byHostname) Len() int { return len(n) }
func (n byHostname) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byHostname) Less(i, j int) bool {
return sortorder.NaturalLess(n[i].Description.Hostname, n[j].Description.Hostname)
}
type listOptions struct {
quiet bool
format string
@ -80,6 +71,8 @@ func runList(dockerCli command.Cli, options listOptions) error {
Output: dockerCli.Out(),
Format: formatter.NewNodeFormat(format, options.quiet),
}
sort.Sort(byHostname(nodes))
sort.Slice(nodes, func(i, j int) bool {
return sortorder.NaturalLess(nodes[i].Description.Hostname, nodes[j].Description.Hostname)
})
return formatter.NodeWrite(nodesCtx, nodes, info)
}

View File

@ -5,6 +5,7 @@ import (
"io"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
)
@ -14,6 +15,8 @@ type fakeClient struct {
pluginDisableFunc func(name string, disableOptions types.PluginDisableOptions) error
pluginEnableFunc func(name string, options types.PluginEnableOptions) error
pluginRemoveFunc func(name string, options types.PluginRemoveOptions) error
pluginInstallFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
pluginListFunc func(filter filters.Args) (types.PluginsListResponse, error)
}
func (c *fakeClient) PluginCreate(ctx context.Context, createContext io.Reader, createOptions types.PluginCreateOptions) error {
@ -43,3 +46,18 @@ func (c *fakeClient) PluginRemove(context context.Context, name string, removeOp
}
return nil
}
func (c *fakeClient) PluginInstall(context context.Context, name string, installOptions types.PluginInstallOptions) (io.ReadCloser, error) {
if c.pluginInstallFunc != nil {
return c.pluginInstallFunc(name, installOptions)
}
return nil, nil
}
func (c *fakeClient) PluginList(context context.Context, filter filters.Args) (types.PluginsListResponse, error) {
if c.pluginListFunc != nil {
return c.pluginListFunc(filter)
}
return types.PluginsListResponse{}, nil
}

View File

@ -151,7 +151,7 @@ func runInstall(dockerCli command.Cli, opts pluginOptions) error {
responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options)
if err != nil {
if strings.Contains(err.Error(), "(image) when fetching") {
return errors.New(err.Error() + " - Use `docker image pull`")
return errors.New(err.Error() + " - Use \"docker image pull\"")
}
return err
}

View File

@ -0,0 +1,141 @@
package plugin
import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/docker/docker/api/types"
"gotest.tools/assert"
)
func TestInstallErrors(t *testing.T) {
testCases := []struct {
description string
args []string
expectedError string
installFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
}{
{
description: "insufficient number of arguments",
args: []string{},
expectedError: "requires at least 1 argument",
},
{
description: "invalid alias",
args: []string{"foo", "--alias", "UPPERCASE_ALIAS"},
expectedError: "invalid",
},
{
description: "invalid plugin name",
args: []string{"UPPERCASE_REPONAME"},
expectedError: "invalid",
},
{
description: "installation error",
args: []string{"foo"},
expectedError: "Error installing plugin",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, fmt.Errorf("Error installing plugin")
},
},
{
description: "installation error due to missing image",
args: []string{"foo"},
expectedError: "docker image pull",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, fmt.Errorf("(image) when fetching")
},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
cmd := newInstallCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestInstallContentTrustErrors(t *testing.T) {
testCases := []struct {
description string
args []string
expectedError string
notaryFunc test.NotaryClientFuncType
}{
{
description: "install plugin, offline notary server",
args: []string{"plugin:tag"},
expectedError: "client is offline",
notaryFunc: notary.GetOfflineNotaryRepository,
},
{
description: "install plugin, uninitialized notary server",
args: []string{"plugin:tag"},
expectedError: "remote trust data does not exist",
notaryFunc: notary.GetUninitializedNotaryRepository,
},
{
description: "install plugin, empty notary server",
args: []string{"plugin:tag"},
expectedError: "No valid trust data for tag",
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{
pluginInstallFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return nil, fmt.Errorf("should not try to install plugin")
},
}, test.EnableContentTrust)
cli.SetNotaryClient(tc.notaryFunc)
cmd := newInstallCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestInstall(t *testing.T) {
testCases := []struct {
description string
args []string
expectedOutput string
installFunc func(name string, options types.PluginInstallOptions) (io.ReadCloser, error)
}{
{
description: "install with no additional flags",
args: []string{"foo"},
expectedOutput: "Installed plugin foo\n",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
return ioutil.NopCloser(strings.NewReader("")), nil
},
},
{
description: "install with disable flag",
args: []string{"--disable", "foo"},
expectedOutput: "Installed plugin foo\n",
installFunc: func(name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
assert.Check(t, options.Disabled)
return ioutil.NopCloser(strings.NewReader("")), nil
},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginInstallFunc: tc.installFunc})
cmd := newInstallCommand(cli)
cmd.SetArgs(tc.args)
assert.NilError(t, cmd.Execute())
assert.Check(t, strings.Contains(cli.OutBuffer().String(), tc.expectedOutput))
}
}

View File

@ -2,12 +2,14 @@ package plugin
import (
"context"
"sort"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type listOptions struct {
@ -46,6 +48,10 @@ func runList(dockerCli command.Cli, options listOptions) error {
return err
}
sort.Slice(plugins, func(i, j int) bool {
return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
})
format := options.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().PluginsFormat) > 0 && !options.quiet {

View File

@ -0,0 +1,174 @@
package plugin
import (
"fmt"
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/golden"
)
func TestListErrors(t *testing.T) {
testCases := []struct {
description string
args []string
flags map[string]string
expectedError string
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
}{
{
description: "too many arguments",
args: []string{"foo"},
expectedError: "accepts no arguments",
},
{
description: "error listing plugins",
args: []string{},
expectedError: "error listing plugins",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{}, fmt.Errorf("error listing plugins")
},
},
{
description: "invalid format",
args: []string{},
flags: map[string]string{
"format": "{{invalid format}}",
},
expectedError: "Template parsing error",
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestList(t *testing.T) {
singlePluginListFunc := func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "id-foo",
Name: "name-foo",
Enabled: true,
Config: types.PluginConfig{
Description: "desc-bar",
},
},
}, nil
}
testCases := []struct {
description string
args []string
flags map[string]string
golden string
listFunc func(filter filters.Args) (types.PluginsListResponse, error)
}{
{
description: "list with no additional flags",
args: []string{},
golden: "plugin-list-without-format.golden",
listFunc: singlePluginListFunc,
},
{
description: "list with filters",
args: []string{},
flags: map[string]string{
"filter": "foo=bar",
},
golden: "plugin-list-without-format.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
assert.Check(t, is.Equal("bar", filter.Get("foo")[0]))
return singlePluginListFunc(filter)
},
},
{
description: "list with quiet option",
args: []string{},
flags: map[string]string{
"quiet": "true",
},
golden: "plugin-list-with-quiet-option.golden",
listFunc: singlePluginListFunc,
},
{
description: "list with no-trunc option",
args: []string{},
flags: map[string]string{
"no-trunc": "true",
"format": "{{ .ID }}",
},
golden: "plugin-list-with-no-trunc-option.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd",
Name: "name-foo",
Enabled: true,
Config: types.PluginConfig{
Description: "desc-bar",
},
},
}, nil
},
},
{
description: "list with format",
args: []string{},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "plugin-list-with-format.golden",
listFunc: singlePluginListFunc,
},
{
description: "list output is sorted based on plugin name",
args: []string{},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "plugin-list-sort.golden",
listFunc: func(filter filters.Args) (types.PluginsListResponse, error) {
return types.PluginsListResponse{
{
ID: "id-1",
Name: "plugin-1-foo",
},
{
ID: "id-2",
Name: "plugin-10-foo",
},
{
ID: "id-3",
Name: "plugin-2-foo",
},
}, nil
},
},
}
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{pluginListFunc: tc.listFunc})
cmd := newListCommand(cli)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
}
}

View File

@ -0,0 +1,3 @@
plugin-1-foo
plugin-2-foo
plugin-10-foo

View File

@ -0,0 +1 @@
name-foo

View File

@ -0,0 +1 @@
xyg4z2hiSLO5yTnBJfg4OYia9gKA6Qjd

View File

@ -0,0 +1 @@
id-foo

View File

@ -0,0 +1,2 @@
ID NAME DESCRIPTION ENABLED
id-foo name-foo desc-bar true

View File

@ -11,6 +11,7 @@ import (
"runtime"
"strings"
"github.com/docker/cli/cli/debug"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
@ -26,9 +27,10 @@ func ElectAuthServer(ctx context.Context, cli Cli) string {
// example a Linux client might be interacting with a Windows daemon, hence
// the default registry URL might be Windows specific.
serverAddress := registry.IndexServer
if info, err := cli.Client().Info(ctx); err != nil {
if info, err := cli.Client().Info(ctx); err != nil && debug.IsEnabled() {
// Only report the warning if we're in debug mode to prevent nagging during engine initialization workflows
fmt.Fprintf(cli.Err(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress)
} else if info.IndexServerAddress == "" {
} else if info.IndexServerAddress == "" && debug.IsEnabled() {
fmt.Fprintf(cli.Err(), "Warning: Empty registry endpoint from daemon. Using system default: %s\n", serverAddress)
} else {
serverAddress = info.IndexServerAddress

View File

@ -125,6 +125,11 @@ func runLogin(dockerCli command.Cli, opts loginOptions) error { //nolint: gocycl
}
response, err = clnt.RegistryLogin(ctx, *authConfig)
if err != nil && client.IsErrConnectionFailed(err) {
// If the server isn't responding (yet) attempt to login purely client side
response, err = loginClientSide(ctx, *authConfig)
}
// If we (still) have an error, give up
if err != nil {
return err
}
@ -167,3 +172,17 @@ func loginWithCredStoreCreds(ctx context.Context, dockerCli command.Cli, authCon
}
return response, err
}
func loginClientSide(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
svc, err := registry.NewService(registry.ServiceOptions{})
if err != nil {
return registrytypes.AuthenticateOKBody{}, err
}
status, token, err := svc.Auth(ctx, &auth, command.UserAgent())
return registrytypes.AuthenticateOKBody{
Status: status,
IdentityToken: token,
}, err
}

View File

@ -28,7 +28,6 @@ type fakeClient struct {
client.Client
}
// nolint: unparam
func (c fakeClient) RegistryLogin(ctx context.Context, auth types.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
if auth.Password == expiredPassword {
return registrytypes.AuthenticateOKBody{}, fmt.Errorf("Invalid Username or Password")

View File

@ -9,7 +9,6 @@ import (
"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/registry"
"github.com/spf13/cobra"
)
@ -81,13 +80,14 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
clnt := dockerCli.Client()
unorderedResults, err := clnt.ImageSearch(ctx, options.term, searchOptions)
results, err := clnt.ImageSearch(ctx, options.term, searchOptions)
if err != nil {
return err
}
results := searchResultsByStars(unorderedResults)
sort.Sort(results)
sort.Slice(results, func(i, j int) bool {
return results[j].StarCount < results[i].StarCount
})
searchCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewSearchFormat(options.format),
@ -95,10 +95,3 @@ func runSearch(dockerCli command.Cli, options searchOptions) error {
}
return formatter.SearchWrite(searchCtx, results, options.automated, int(options.stars))
}
// searchResultsByStars sorts search results in descending order by number of stars.
type searchResultsByStars []registrytypes.SearchResult
func (r searchResultsByStars) Len() int { return len(r) }
func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount }

View File

@ -13,6 +13,7 @@ import (
// Prevents a circular import with "github.com/docker/cli/internal/test"
. "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/debug"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
@ -78,6 +79,8 @@ func TestElectAuthServer(t *testing.T) {
},
},
}
// Enable debug to see warnings we're checking for
debug.Enable()
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{infoFunc: tc.infoFunc})
server := ElectAuthServer(context.Background(), cli)

View File

@ -9,19 +9,10 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/spf13/cobra"
"vbom.ml/util/sortorder"
)
type bySecretName []swarm.Secret
func (r bySecretName) Len() int { return len(r) }
func (r bySecretName) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r bySecretName) Less(i, j int) bool {
return sortorder.NaturalLess(r[i].Spec.Name, r[j].Spec.Name)
}
type listOptions struct {
quiet bool
format string
@ -66,7 +57,9 @@ func runSecretList(dockerCli command.Cli, options listOptions) error {
}
}
sort.Sort(bySecretName(secrets))
sort.Slice(secrets, func(i, j int) bool {
return sortorder.NaturalLess(secrets[i].Spec.Name, secrets[j].Spec.Name)
})
secretCtx := formatter.Context{
Output: dockerCli.Out(),

View File

@ -44,12 +44,6 @@ func newListCommand(dockerCli command.Cli) *cobra.Command {
return cmd
}
type byName []swarm.Service
func (n byName) Len() int { return len(n) }
func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n byName) Less(i, j int) bool { return sortorder.NaturalLess(n[i].Spec.Name, n[j].Spec.Name) }
func runList(dockerCli command.Cli, options listOptions) error {
ctx := context.Background()
client := dockerCli.Client()
@ -60,7 +54,9 @@ func runList(dockerCli command.Cli, options listOptions) error {
return err
}
sort.Sort(byName(services))
sort.Slice(services, func(i, j int) bool {
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
})
info := map[string]formatter.ServiceListInfo{}
if len(services) > 0 && !options.quiet {
// only non-empty services and not quiet, should we call TaskList and NodeList api

View File

@ -598,7 +598,9 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N
}
networks[i].Target = nwID
}
sort.Sort(byNetworkTarget(networks))
sort.Slice(networks, func(i, j int) bool {
return networks[i].Target < networks[j].Target
})
resources, err := options.resources.ToResourceRequirements()
if err != nil {

View File

@ -140,7 +140,7 @@ loop:
}
func updateNodeFilter(ctx context.Context, client client.APIClient, filter filters.Args) error {
if filter.Include("node") {
if filter.Contains("node") {
nodeFilters := filter.Get("node")
for _, nodeFilter := range nodeFilters {
nodeReference, err := node.Reference(ctx, client, nodeFilter)

View File

@ -753,20 +753,6 @@ func removeItems(
return newSeq
}
type byMountSource []mounttypes.Mount
func (m byMountSource) Len() int { return len(m) }
func (m byMountSource) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m byMountSource) Less(i, j int) bool {
a, b := m[i], m[j]
if a.Source == b.Source {
return a.Target < b.Target
}
return a.Source < b.Source
}
func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
mountsByTarget := map[string]mounttypes.Mount{}
@ -796,7 +782,15 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error {
newMounts = append(newMounts, mount)
}
}
sort.Sort(byMountSource(newMounts))
sort.Slice(newMounts, func(i, j int) bool {
a, b := newMounts[i], newMounts[j]
if a.Source == b.Source {
return a.Target < b.Target
}
return a.Source < b.Source
})
*mounts = newMounts
return nil
}
@ -886,16 +880,6 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error {
return nil
}
type byPortConfig []swarm.PortConfig
func (r byPortConfig) Len() int { return len(r) }
func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r byPortConfig) Less(i, j int) bool {
// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
// In updatePorts we already filter out with map so there is duplicate entries
return portConfigToString(&r[i]) < portConfigToString(&r[j])
}
func portConfigToString(portConfig *swarm.PortConfig) string {
protocol := portConfig.Protocol
mode := portConfig.PublishMode
@ -944,7 +928,11 @@ portLoop:
}
// Sort the PortConfig to avoid unnecessary updates
sort.Sort(byPortConfig(newPorts))
sort.Slice(newPorts, func(i, j int) bool {
// We convert PortConfig into `port/protocol`, e.g., `80/tcp`
// In updatePorts we already filter out with map so there is duplicate entries
return portConfigToString(&newPorts[i]) < portConfigToString(&newPorts[j])
})
*portConfig = newPorts
return nil
}
@ -1142,14 +1130,6 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec)
return nil
}
type byNetworkTarget []swarm.NetworkAttachmentConfig
func (m byNetworkTarget) Len() int { return len(m) }
func (m byNetworkTarget) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m byNetworkTarget) Less(i, j int) bool {
return m[i].Target < m[j].Target
}
func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flags *pflag.FlagSet, spec *swarm.ServiceSpec) error {
// spec.TaskTemplate.Networks takes precedence over the deprecated
// spec.Networks field. If spec.Network is in use, we'll migrate those
@ -1198,7 +1178,9 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag
}
}
sort.Sort(byNetworkTarget(newNetworks))
sort.Slice(newNetworks, func(i, j int) bool {
return newNetworks[i].Target < newNetworks[j].Target
})
spec.TaskTemplate.Networks = newNetworks
return nil

View File

@ -81,7 +81,7 @@ func RunDeploy(dockerCli command.Cli, flags *pflag.FlagSet, config *composetypes
case commonOrchestrator.HasKubernetes():
kli, err := kubernetes.WrapCli(dockerCli, kubernetes.NewOptions(flags, commonOrchestrator))
if err != nil {
return err
return errors.Wrap(err, "unable to deploy to Kubernetes")
}
return kubernetes.RunDeploy(kli, opts, config)
default:

View File

@ -4,10 +4,12 @@ import (
"fmt"
"net"
"net/url"
"os"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/kubernetes"
cliv1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1"
"github.com/pkg/errors"
flag "github.com/spf13/pflag"
kubeclient "k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
@ -58,7 +60,10 @@ func WrapCli(dockerCli command.Cli, opts Options) (*KubeCli, error) {
cli.kubeNamespace = opts.Namespace
if opts.Namespace == "" {
configNamespace, _, err := clientConfig.Namespace()
if err != nil {
switch {
case os.IsNotExist(err), os.IsPermission(err):
return nil, errors.Wrap(err, "unable to load configuration file")
case err != nil:
return nil, err
}
cli.kubeNamespace = configNamespace

View File

@ -8,13 +8,72 @@ import (
"strings"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composeTypes "github.com/docker/cli/cli/compose/types"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/compose/v1beta2"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NewStackConverter returns a converter from types.Config (compose) to the specified
// stack version or error out if the version is not supported or existent.
func NewStackConverter(version string) (StackConverter, error) {
switch version {
case "v1beta1":
return stackV1Beta1Converter{}, nil
case "v1beta2":
return stackV1Beta2Converter{}, nil
default:
return nil, errors.Errorf("stack version %s unsupported", version)
}
}
// StackConverter converts a compose types.Config to a Stack
type StackConverter interface {
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error)
}
type stackV1Beta1Converter struct{}
func (s stackV1Beta1Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
cfg.Version = v1beta1.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg)
if err != nil {
return Stack{}, err
}
res, err := yaml.Marshal(cfg)
if err != nil {
return Stack{}, err
}
// reload the result to check that it produced a valid 3.5 compose file
resparsedConfig, err := loader.ParseYAML(res)
if err != nil {
return Stack{}, err
}
if err = schema.Validate(resparsedConfig, v1beta1.MaxComposeVersion); err != nil {
return Stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1.MaxComposeVersion)
}
st.ComposeFile = string(res)
return st, nil
}
type stackV1Beta2Converter struct{}
func (s stackV1Beta2Converter) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return fromCompose(stderr, name, cfg)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (Stack, error) {
return Stack{
Name: name,
Spec: fromComposeConfig(stderr, cfg),
}, nil
}
func loadStackData(composefile string) (*composetypes.Config, error) {
parsed, err := loader.ParseYAML([]byte(composefile))
if err != nil {
@ -30,44 +89,44 @@ func loadStackData(composefile string) (*composetypes.Config, error) {
}
// Conversions from internal stack to different stack compose component versions.
func stackFromV1beta1(in *v1beta1.Stack) (stack, error) {
func stackFromV1beta1(in *v1beta1.Stack) (Stack, error) {
cfg, err := loadStackData(in.Spec.ComposeFile)
if err != nil {
return stack{}, err
return Stack{}, err
}
return stack{
name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace,
composeFile: in.Spec.ComposeFile,
spec: fromComposeConfig(ioutil.Discard, cfg),
return Stack{
Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace,
ComposeFile: in.Spec.ComposeFile,
Spec: fromComposeConfig(ioutil.Discard, cfg),
}, nil
}
func stackToV1beta1(s stack) *v1beta1.Stack {
func stackToV1beta1(s Stack) *v1beta1.Stack {
return &v1beta1.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Name: s.Name,
},
Spec: v1beta1.StackSpec{
ComposeFile: s.composeFile,
ComposeFile: s.ComposeFile,
},
}
}
func stackFromV1beta2(in *v1beta2.Stack) stack {
return stack{
name: in.ObjectMeta.Name,
namespace: in.ObjectMeta.Namespace,
spec: in.Spec,
func stackFromV1beta2(in *v1beta2.Stack) Stack {
return Stack{
Name: in.ObjectMeta.Name,
Namespace: in.ObjectMeta.Namespace,
Spec: in.Spec,
}
}
func stackToV1beta2(s stack) *v1beta2.Stack {
func stackToV1beta2(s Stack) *v1beta2.Stack {
return &v1beta2.Stack{
ObjectMeta: metav1.ObjectMeta{
Name: s.name,
Name: s.Name,
},
Spec: s.spec,
Spec: s.Spec,
}
}

View File

@ -0,0 +1,18 @@
package kubernetes
import (
"testing"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestNewStackConverter(t *testing.T) {
_, err := NewStackConverter("v1alpha1")
assert.Check(t, is.ErrorContains(err, "stack version v1alpha1 unsupported"))
_, err = NewStackConverter("v1beta1")
assert.NilError(t, err)
_, err = NewStackConverter("v1beta2")
assert.NilError(t, err)
}

View File

@ -70,13 +70,13 @@ func RunDeploy(dockerCli *KubeCli, opts options.Deploy, cfg *composetypes.Config
}
}()
err = watcher.Watch(stack.name, stack.getServices(), statusUpdates)
err = watcher.Watch(stack.Name, stack.getServices(), statusUpdates)
close(statusUpdates)
<-displayDone
if err != nil {
return err
}
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.name)
fmt.Fprintf(cmdOut, "\nStack %s is stable and running\n\n", stack.Name)
return nil
}

View File

@ -48,10 +48,10 @@ func getStacks(kubeCli *KubeCli, opts options.List) ([]*formatter.Stack, error)
var formattedStacks []*formatter.Stack
for _, stack := range stacks {
formattedStacks = append(formattedStacks, &formatter.Stack{
Name: stack.name,
Name: stack.Name,
Services: len(stack.getServices()),
Orchestrator: "Kubernetes",
Namespace: stack.namespace,
Namespace: stack.Namespace,
})
}
return formattedStacks, nil

View File

@ -12,18 +12,18 @@ import (
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type stack struct {
name string
namespace string
composeFile string
spec *v1beta2.StackSpec
// Stack is the main type used by stack commands so they remain independent from kubernetes compose component version.
type Stack struct {
Name string
Namespace string
ComposeFile string
Spec *v1beta2.StackSpec
}
// getServices returns all the stack service names, sorted lexicographically
func (s *stack) getServices() []string {
services := make([]string, len(s.spec.Services))
for i, service := range s.spec.Services {
func (s *Stack) getServices() []string {
services := make([]string, len(s.Spec.Services))
for i, service := range s.Spec.Services {
services[i] = service.Name
}
sort.Strings(services)
@ -31,8 +31,8 @@ func (s *stack) getServices() []string {
}
// createFileBasedConfigMaps creates a Kubernetes ConfigMap for each Compose global file-based config.
func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.spec.Configs {
func (s *Stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface) error {
for name, config := range s.Spec.Configs {
if config.File == "" {
continue
}
@ -43,7 +43,7 @@ func (s *stack) createFileBasedConfigMaps(configMaps corev1.ConfigMapInterface)
return err
}
if _, err := configMaps.Create(toConfigMap(s.name, name, fileName, content)); err != nil {
if _, err := configMaps.Create(toConfigMap(s.Name, name, fileName, content)); err != nil {
return err
}
}
@ -71,8 +71,8 @@ func toConfigMap(stackName, name, key string, content []byte) *apiv1.ConfigMap {
}
// createFileBasedSecrets creates a Kubernetes Secret for each Compose global file-based secret.
func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.spec.Secrets {
func (s *Stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
for name, secret := range s.Spec.Secrets {
if secret.File == "" {
continue
}
@ -83,7 +83,7 @@ func (s *stack) createFileBasedSecrets(secrets corev1.SecretInterface) error {
return err
}
if _, err := secrets.Create(toSecret(s.name, name, fileName, content)); err != nil {
if _, err := secrets.Create(toSecret(s.Name, name, fileName, content)); err != nil {
return err
}
}

View File

@ -2,17 +2,10 @@ package kubernetes
import (
"fmt"
"io"
"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
composetypes "github.com/docker/cli/cli/compose/types"
composev1beta1 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta1"
composev1beta2 "github.com/docker/cli/kubernetes/client/clientset/typed/compose/v1beta2"
v1beta1types "github.com/docker/cli/kubernetes/compose/v1beta1"
"github.com/docker/cli/kubernetes/labels"
"github.com/pkg/errors"
yaml "gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
@ -20,16 +13,17 @@ import (
// StackClient talks to a kubernetes compose component.
type StackClient interface {
CreateOrUpdate(s stack) error
StackConverter
CreateOrUpdate(s Stack) error
Delete(name string) error
Get(name string) (stack, error)
List(opts metav1.ListOptions) ([]stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s stack) error
FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error)
Get(name string) (Stack, error)
List(opts metav1.ListOptions) ([]Stack, error)
IsColliding(servicesClient corev1.ServiceInterface, s Stack) error
}
// stackV1Beta1 implements stackClient interface and talks to compose component v1beta1.
type stackV1Beta1 struct {
stackV1Beta1Converter
stacks composev1beta1.StackInterface
}
@ -41,10 +35,10 @@ func newStackV1Beta1(config *rest.Config, namespace string) (*stackV1Beta1, erro
return &stackV1Beta1{stacks: client.Stacks(namespace)}, nil
}
func (s *stackV1Beta1) CreateOrUpdate(internalStack stack) error {
func (s *stackV1Beta1) CreateOrUpdate(internalStack Stack) error {
// If it already exists, update the stack
if stackBeta1, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.composeFile
if stackBeta1, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta1.Spec.ComposeFile = internalStack.ComposeFile
_, err := s.stacks.Update(stackBeta1)
return err
}
@ -57,20 +51,20 @@ func (s *stackV1Beta1) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{})
}
func (s *stackV1Beta1) Get(name string) (stack, error) {
func (s *stackV1Beta1) Get(name string) (Stack, error) {
stackBeta1, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil {
return stack{}, err
return Stack{}, err
}
return stackFromV1beta1(stackBeta1)
}
func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) {
func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]Stack, error) {
list, err := s.stacks.List(opts)
if err != nil {
return nil, err
}
stacks := make([]stack, len(list.Items))
stacks := make([]Stack, len(list.Items))
for i := range list.Items {
stack, err := stackFromV1beta1(&list.Items[i])
if err != nil {
@ -82,9 +76,9 @@ func (s *stackV1Beta1) List(opts metav1.ListOptions) ([]stack, error) {
}
// IsColliding verifies that services defined in the stack collides with already deployed services
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
func (s *stackV1Beta1) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error {
for _, srv := range st.getServices() {
if err := verify(servicesClient, st.name, srv); err != nil {
if err := verify(servicesClient, st.Name, srv); err != nil {
return err
}
}
@ -108,31 +102,9 @@ func verify(services corev1.ServiceInterface, stackName string, service string)
return nil
}
func (s *stackV1Beta1) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
cfg.Version = v1beta1types.MaxComposeVersion
st, err := fromCompose(stderr, name, cfg)
if err != nil {
return stack{}, err
}
res, err := yaml.Marshal(cfg)
if err != nil {
return stack{}, err
}
// reload the result to check that it produced a valid 3.5 compose file
resparsedConfig, err := loader.ParseYAML(res)
if err != nil {
return stack{}, err
}
if err = schema.Validate(resparsedConfig, v1beta1types.MaxComposeVersion); err != nil {
return stack{}, errors.Wrapf(err, "the compose yaml file is invalid with v%s", v1beta1types.MaxComposeVersion)
}
st.composeFile = string(res)
return st, nil
}
// stackV1Beta2 implements stackClient interface and talks to compose component v1beta2.
type stackV1Beta2 struct {
stackV1Beta2Converter
stacks composev1beta2.StackInterface
}
@ -144,10 +116,10 @@ func newStackV1Beta2(config *rest.Config, namespace string) (*stackV1Beta2, erro
return &stackV1Beta2{stacks: client.Stacks(namespace)}, nil
}
func (s *stackV1Beta2) CreateOrUpdate(internalStack stack) error {
func (s *stackV1Beta2) CreateOrUpdate(internalStack Stack) error {
// If it already exists, update the stack
if stackBeta2, err := s.stacks.Get(internalStack.name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.spec
if stackBeta2, err := s.stacks.Get(internalStack.Name, metav1.GetOptions{}); err == nil {
stackBeta2.Spec = internalStack.Spec
_, err := s.stacks.Update(stackBeta2)
return err
}
@ -160,20 +132,20 @@ func (s *stackV1Beta2) Delete(name string) error {
return s.stacks.Delete(name, &metav1.DeleteOptions{})
}
func (s *stackV1Beta2) Get(name string) (stack, error) {
func (s *stackV1Beta2) Get(name string) (Stack, error) {
stackBeta2, err := s.stacks.Get(name, metav1.GetOptions{})
if err != nil {
return stack{}, err
return Stack{}, err
}
return stackFromV1beta2(stackBeta2), nil
}
func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) {
func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]Stack, error) {
list, err := s.stacks.List(opts)
if err != nil {
return nil, err
}
stacks := make([]stack, len(list.Items))
stacks := make([]Stack, len(list.Items))
for i := range list.Items {
stacks[i] = stackFromV1beta2(&list.Items[i])
}
@ -181,17 +153,6 @@ func (s *stackV1Beta2) List(opts metav1.ListOptions) ([]stack, error) {
}
// IsColliding is handle server side with the compose api v1beta2, so nothing to do here
func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st stack) error {
func (s *stackV1Beta2) IsColliding(servicesClient corev1.ServiceInterface, st Stack) error {
return nil
}
func (s *stackV1Beta2) FromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
return fromCompose(stderr, name, cfg)
}
func fromCompose(stderr io.Writer, name string, cfg *composetypes.Config) (stack, error) {
return stack{
name: name,
spec: fromComposeConfig(stderr, cfg),
}, nil
}

View File

@ -25,18 +25,14 @@ func TestFromCompose(t *testing.T) {
},
})
assert.NilError(t, err)
assert.Equal(t, "foo", s.name)
assert.Equal(t, "foo", s.Name)
assert.Equal(t, string(`version: "3.5"
services:
bar:
image: bar
foo:
image: foo
networks: {}
volumes: {}
secrets: {}
configs: {}
`), s.composeFile)
`), s.ComposeFile)
}
func TestFromComposeUnsupportedVersion(t *testing.T) {

View File

@ -51,121 +51,134 @@ func TestStackPsErrors(t *testing.T) {
}
}
func TestRunPSWithEmptyName(t *testing.T) {
cmd := newPsCommand(test.NewFakeCli(&fakeClient{}), &orchestrator)
cmd.SetArgs([]string{"' '"})
cmd.SetOutput(ioutil.Discard)
func TestStackPs(t *testing.T) {
testCases := []struct {
doc string
taskListFunc func(types.TaskListOptions) ([]swarm.Task, error)
nodeInspectWithRaw func(string) (swarm.Node, []byte, error)
config configfile.ConfigFile
args []string
flags map[string]string
expectedErr string
golden string
}{
{
doc: "WithEmptyName",
args: []string{"' '"},
expectedErr: `invalid stack name: "' '"`,
},
{
doc: "WithEmptyStack",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{}, nil
},
args: []string{"foo"},
expectedErr: "nothing found in stack: foo",
},
{
doc: "WithQuietOption",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("id-foo"))}, nil
},
args: []string{"foo"},
flags: map[string]string{
"quiet": "true",
},
golden: "stack-ps-with-quiet-option.golden",
},
{
doc: "WithNoTruncOption",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil
},
args: []string{"foo"},
flags: map[string]string{
"no-trunc": "true",
"format": "{{ .ID }}",
},
golden: "stack-ps-with-no-trunc-option.golden",
},
{
doc: "WithNoResolveOption",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskNodeID("id-node-foo"),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
args: []string{"foo"},
flags: map[string]string{
"no-resolve": "true",
"format": "{{ .Node }}",
},
golden: "stack-ps-with-no-resolve-option.golden",
},
{
doc: "WithFormat",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
args: []string{"foo"},
flags: map[string]string{
"format": "{{ .Name }}",
},
golden: "stack-ps-with-format.golden",
},
{
doc: "WithConfigFormat",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
config: configfile.ConfigFile{
TasksFormat: "{{ .Name }}",
},
args: []string{"foo"},
golden: "stack-ps-with-config-format.golden",
},
{
doc: "WithoutFormat",
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskID("id-foo"),
TaskServiceID("service-id-foo"),
TaskNodeID("id-node"),
WithTaskSpec(TaskImage("myimage:mytag")),
TaskDesiredState(swarm.TaskStateReady),
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
args: []string{"foo"},
golden: "stack-ps-without-format.golden",
},
}
assert.ErrorContains(t, cmd.Execute(), `invalid stack name: "' '"`)
}
func TestStackPsEmptyStack(t *testing.T) {
fakeCli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{}, nil
},
})
cmd := newPsCommand(fakeCli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.SetOutput(ioutil.Discard)
assert.Error(t, cmd.Execute(), "nothing found in stack: foo")
assert.Check(t, is.Equal("", fakeCli.OutBuffer().String()))
}
func TestStackPsWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("id-foo"))}, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("quiet", "true")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-quiet-option.golden")
}
func TestStackPsWithNoTruncOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskID("xn4cypcov06f2w8gsbaf2lst3"))}, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("no-trunc", "true")
cmd.Flags().Set("format", "{{ .ID }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-no-trunc-option.golden")
}
func TestStackPsWithNoResolveOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskNodeID("id-node-foo"),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("no-resolve", "true")
cmd.Flags().Set("format", "{{ .Node }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-no-resolve-option.golden")
}
func TestStackPsWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
cmd.Flags().Set("format", "{{ .Name }}")
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-format.golden")
}
func TestStackPsWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(TaskServiceID("service-id-foo"))}, nil
},
})
cli.SetConfigFile(&configfile.ConfigFile{
TasksFormat: "{{ .Name }}",
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-with-config-format.golden")
}
func TestStackPsWithoutFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) {
return []swarm.Task{*Task(
TaskID("id-foo"),
TaskServiceID("service-id-foo"),
TaskNodeID("id-node"),
WithTaskSpec(TaskImage("myimage:mytag")),
TaskDesiredState(swarm.TaskStateReady),
WithStatus(TaskState(swarm.TaskStateFailed), Timestamp(time.Now().Add(-2*time.Hour))),
)}, nil
},
nodeInspectWithRaw: func(ref string) (swarm.Node, []byte, error) {
return *Node(NodeName("node-name-bar")), nil, nil
},
})
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs([]string{"foo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "stack-ps-without-format.golden")
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{
taskListFunc: tc.taskListFunc,
nodeInspectWithRaw: tc.nodeInspectWithRaw,
})
cli.SetConfigFile(&tc.config)
cmd := newPsCommand(cli, &orchestrator)
cmd.SetArgs(tc.args)
for key, value := range tc.flags {
cmd.Flags().Set(key, value)
}
cmd.SetOutput(ioutil.Discard)
if tc.expectedErr != "" {
assert.Error(t, cmd.Execute(), tc.expectedErr)
assert.Check(t, is.Equal("", cli.OutBuffer().String()))
return
}
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
})
}
}

View File

@ -3,6 +3,7 @@ package swarm
import (
"context"
"fmt"
"net"
"strings"
"github.com/docker/cli/cli"
@ -17,10 +18,12 @@ type initOptions struct {
swarmOptions
listenAddr NodeAddrOption
// Not a NodeAddrOption because it has no default port.
advertiseAddr string
dataPathAddr string
forceNewCluster bool
availability string
advertiseAddr string
dataPathAddr string
forceNewCluster bool
availability string
defaultAddrPools []net.IPNet
DefaultAddrPoolMaskLength uint32
}
func newInitCommand(dockerCli command.Cli) *cobra.Command {
@ -41,24 +44,36 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|interface>[:port])")
flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|interface>[:port])")
flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: <ip|interface>)")
flags.SetAnnotation(flagDataPathAddr, "version", []string{"1.31"})
flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state")
flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)")
flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`)
flags.IPNetSliceVar(&opts.defaultAddrPools, flagDefaultAddrPool, []net.IPNet{}, "default address pool in CIDR format")
flags.SetAnnotation(flagDefaultAddrPool, "version", []string{"1.39"})
flags.Uint32Var(&opts.DefaultAddrPoolMaskLength, flagDefaultAddrPoolMaskLength, 24, "default address pool subnet mask length")
flags.SetAnnotation(flagDefaultAddrPoolMaskLength, "version", []string{"1.39"})
addSwarmFlags(flags, &opts.swarmOptions)
return cmd
}
func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) error {
var defaultAddrPool []string
client := dockerCli.Client()
ctx := context.Background()
for _, p := range opts.defaultAddrPools {
defaultAddrPool = append(defaultAddrPool, p.String())
}
req := swarm.InitRequest{
ListenAddr: opts.listenAddr.String(),
AdvertiseAddr: opts.advertiseAddr,
DataPathAddr: opts.dataPathAddr,
DefaultAddrPool: defaultAddrPool,
ForceNewCluster: opts.forceNewCluster,
Spec: opts.swarmOptions.ToSpec(flags),
AutoLockManagers: opts.swarmOptions.autolock,
SubnetSize: opts.DefaultAddrPoolMaskLength,
}
if flags.Changed(flagAvailability) {
availability := swarm.NodeAvailability(strings.ToLower(opts.availability))

View File

@ -42,6 +42,7 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command {
flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: <ip|interface>[:port])")
flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: <ip|interface>[:port])")
flags.StringVar(&opts.dataPathAddr, flagDataPathAddr, "", "Address or interface to use for data path traffic (format: <ip|interface>)")
flags.SetAnnotation(flagDataPathAddr, "version", []string{"1.31"})
flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm")
flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`)
return cmd

View File

@ -17,22 +17,24 @@ import (
const (
defaultListenAddr = "0.0.0.0:2377"
flagCertExpiry = "cert-expiry"
flagDispatcherHeartbeat = "dispatcher-heartbeat"
flagListenAddr = "listen-addr"
flagAdvertiseAddr = "advertise-addr"
flagDataPathAddr = "data-path-addr"
flagQuiet = "quiet"
flagRotate = "rotate"
flagToken = "token"
flagTaskHistoryLimit = "task-history-limit"
flagExternalCA = "external-ca"
flagMaxSnapshots = "max-snapshots"
flagSnapshotInterval = "snapshot-interval"
flagAutolock = "autolock"
flagAvailability = "availability"
flagCACert = "ca-cert"
flagCAKey = "ca-key"
flagCertExpiry = "cert-expiry"
flagDispatcherHeartbeat = "dispatcher-heartbeat"
flagListenAddr = "listen-addr"
flagAdvertiseAddr = "advertise-addr"
flagDataPathAddr = "data-path-addr"
flagDefaultAddrPool = "default-addr-pool"
flagDefaultAddrPoolMaskLength = "default-addr-pool-mask-length"
flagQuiet = "quiet"
flagRotate = "rotate"
flagToken = "token"
flagTaskHistoryLimit = "task-history-limit"
flagExternalCA = "external-ca"
flagMaxSnapshots = "max-snapshots"
flagSnapshotInterval = "snapshot-interval"
flagAutolock = "autolock"
flagAvailability = "availability"
flagCACert = "ca-cert"
flagCAKey = "ca-key"
)
type swarmOptions struct {

View File

@ -19,6 +19,7 @@ func NewSystemCommand(dockerCli command.Cli) *cobra.Command {
NewInfoCommand(dockerCli),
newDiskUsageCommand(dockerCli),
newPruneCommand(dockerCli),
newDialStdioCommand(dockerCli),
)
return cmd

View File

@ -0,0 +1,107 @@
package system
import (
"context"
"io"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// newDialStdioCommand creates a new cobra.Command for `docker system dial-stdio`
func newDialStdioCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "dial-stdio",
Short: "Proxy the stdio stream to the daemon connection. Should not be invoked manually.",
Args: cli.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return runDialStdio(dockerCli)
},
}
return cmd
}
func runDialStdio(dockerCli command.Cli) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dialer := dockerCli.Client().Dialer()
conn, err := dialer(ctx)
if err != nil {
return errors.Wrap(err, "failed to open the raw stream connection")
}
connHalfCloser, ok := conn.(halfCloser)
if !ok {
return errors.New("the raw stream connection does not implement halfCloser")
}
stdin2conn := make(chan error)
conn2stdout := make(chan error)
go func() {
stdin2conn <- copier(connHalfCloser, &halfReadCloserWrapper{os.Stdin}, "stdin to stream")
}()
go func() {
conn2stdout <- copier(&halfWriteCloserWrapper{os.Stdout}, connHalfCloser, "stream to stdout")
}()
select {
case err = <-stdin2conn:
if err != nil {
return err
}
// wait for stdout
err = <-conn2stdout
case err = <-conn2stdout:
// return immediately without waiting for stdin to be closed.
// (stdin is never closed when tty)
}
return err
}
func copier(to halfWriteCloser, from halfReadCloser, debugDescription string) error {
defer func() {
if err := from.CloseRead(); err != nil {
logrus.Errorf("error while CloseRead (%s): %v", debugDescription, err)
}
if err := to.CloseWrite(); err != nil {
logrus.Errorf("error while CloseWrite (%s): %v", debugDescription, err)
}
}()
if _, err := io.Copy(to, from); err != nil {
return errors.Wrapf(err, "error while Copy (%s)", debugDescription)
}
return nil
}
type halfReadCloser interface {
io.Reader
CloseRead() error
}
type halfWriteCloser interface {
io.Writer
CloseWrite() error
}
type halfCloser interface {
halfReadCloser
halfWriteCloser
}
type halfReadCloserWrapper struct {
io.ReadCloser
}
func (x *halfReadCloserWrapper) CloseRead() error {
return x.Close()
}
type halfWriteCloserWrapper struct {
io.WriteCloser
}
func (x *halfWriteCloserWrapper) CloseWrite() error {
return x.Close()
}

View File

@ -206,48 +206,11 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled)
fmt.Fprint(dockerCli.Out(), "\n")
// Only output these warnings if the server does not support these features
if info.OSType != "windows" {
printStorageDriverWarnings(dockerCli, info)
if !info.MemoryLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support")
}
if !info.SwapLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support")
}
if !info.KernelMemory {
fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support")
}
if !info.OomKillDisable {
fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support")
}
if !info.CPUCfsQuota {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support")
}
if !info.CPUCfsPeriod {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support")
}
if !info.CPUShares {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support")
}
if !info.CPUSet {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support")
}
if !info.IPv4Forwarding {
fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled")
}
if !info.BridgeNfIptables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled")
}
if !info.BridgeNfIP6tables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled")
}
}
printWarnings(dockerCli, info)
return nil
}
// nolint: gocyclo
func printSwarmInfo(dockerCli command.Cli, info types.Info) {
if info.Swarm.LocalNodeState == swarm.LocalNodeStateInactive || info.Swarm.LocalNodeState == swarm.LocalNodeStateLocked {
return
@ -261,7 +224,16 @@ func printSwarmInfo(dockerCli command.Cli, info types.Info) {
fmt.Fprintln(dockerCli.Out(), " ClusterID:", info.Swarm.Cluster.ID)
fmt.Fprintln(dockerCli.Out(), " Managers:", info.Swarm.Managers)
fmt.Fprintln(dockerCli.Out(), " Nodes:", info.Swarm.Nodes)
var strAddrPool strings.Builder
if info.Swarm.Cluster.DefaultAddrPool != nil {
for _, p := range info.Swarm.Cluster.DefaultAddrPool {
strAddrPool.WriteString(p + " ")
}
fmt.Fprintln(dockerCli.Out(), " Default Address Pool:", strAddrPool.String())
fmt.Fprintln(dockerCli.Out(), " SubnetSize:", info.Swarm.Cluster.SubnetSize)
}
fmt.Fprintln(dockerCli.Out(), " Orchestration:")
taskHistoryRetentionLimit := int64(0)
if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil {
taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit
@ -305,11 +277,73 @@ func printSwarmInfo(dockerCli command.Cli, info types.Info) {
}
}
func printWarnings(dockerCli command.Cli, info types.Info) {
if len(info.Warnings) > 0 {
fmt.Fprintln(dockerCli.Err(), strings.Join(info.Warnings, "\n"))
return
}
// daemon didn't return warnings. Fallback to old behavior
printStorageDriverWarnings(dockerCli, info)
printWarningsLegacy(dockerCli, info)
}
// printWarningsLegacy generates warnings based on information returned by the daemon.
// DEPRECATED: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printWarningsLegacy(dockerCli command.Cli, info types.Info) {
if info.OSType == "windows" {
return
}
if !info.MemoryLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support")
}
if !info.SwapLimit {
fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support")
}
if !info.KernelMemory {
fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support")
}
if !info.OomKillDisable {
fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support")
}
if !info.CPUCfsQuota {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support")
}
if !info.CPUCfsPeriod {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support")
}
if !info.CPUShares {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support")
}
if !info.CPUSet {
fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support")
}
if !info.IPv4Forwarding {
fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled")
}
if !info.BridgeNfIptables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled")
}
if !info.BridgeNfIP6tables {
fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled")
}
}
// printStorageDriverWarnings generates warnings based on storage-driver information
// returned by the daemon.
// DEPRECATED: warnings are now generated by the daemon, and returned in
// info.Warnings. This function is used to provide backward compatibility with
// daemons that do not provide these warnings. No new warnings should be added
// here.
func printStorageDriverWarnings(dockerCli command.Cli, info types.Info) {
if info.OSType == "windows" {
return
}
if info.DriverStatus == nil {
return
}
for _, pair := range info.DriverStatus {
if pair[0] == "Data loop file" {
fmt.Fprintf(dockerCli.Err(), "WARNING: %s: usage of loopback devices is "+

View File

@ -207,32 +207,59 @@ func TestPrettyPrintInfo(t *testing.T) {
infoWithWarningsLinux.BridgeNfIptables = false
infoWithWarningsLinux.BridgeNfIP6tables = false
sampleInfoDaemonWarnings := sampleInfoNoSwarm
sampleInfoDaemonWarnings.Warnings = []string{
"WARNING: No memory limit support",
"WARNING: No swap limit support",
"WARNING: No kernel memory limit support",
"WARNING: No oom kill disable support",
"WARNING: No cpu cfs quota support",
"WARNING: No cpu cfs period support",
"WARNING: No cpu shares support",
"WARNING: No cpuset support",
"WARNING: IPv4 forwarding is disabled",
"WARNING: bridge-nf-call-iptables is disabled",
"WARNING: bridge-nf-call-ip6tables is disabled",
}
for _, tc := range []struct {
doc string
dockerInfo types.Info
expectedGolden string
warningsGolden string
}{
{
doc: "info without swarm",
dockerInfo: sampleInfoNoSwarm,
expectedGolden: "docker-info-no-swarm",
},
{
doc: "info with swarm",
dockerInfo: infoWithSwarm,
expectedGolden: "docker-info-with-swarm",
},
{
doc: "info with legacy warnings",
dockerInfo: infoWithWarningsLinux,
expectedGolden: "docker-info-no-swarm",
warningsGolden: "docker-info-warnings",
},
{
doc: "info with daemon warnings",
dockerInfo: sampleInfoDaemonWarnings,
expectedGolden: "docker-info-no-swarm",
warningsGolden: "docker-info-warnings",
},
} {
cli := test.NewFakeCli(&fakeClient{})
assert.NilError(t, prettyPrintInfo(cli, tc.dockerInfo))
golden.Assert(t, cli.OutBuffer().String(), tc.expectedGolden+".golden")
if tc.warningsGolden != "" {
golden.Assert(t, cli.ErrBuffer().String(), tc.warningsGolden+".golden")
} else {
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
}
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
assert.NilError(t, prettyPrintInfo(cli, tc.dockerInfo))
golden.Assert(t, cli.OutBuffer().String(), tc.expectedGolden+".golden")
if tc.warningsGolden != "" {
golden.Assert(t, cli.ErrBuffer().String(), tc.warningsGolden+".golden")
} else {
assert.Check(t, is.Equal("", cli.ErrBuffer().String()))
}
})
}
}

View File

@ -68,7 +68,7 @@ func runBuildCachePrune(dockerCli command.Cli, _ opts.FilterOpt) (uint64, string
func runPrune(dockerCli command.Cli, options pruneOptions) error {
// TODO version this once "until" filter is supported for volumes
if options.pruneVolumes && options.filter.Value().Include("until") {
if options.pruneVolumes && options.filter.Value().Contains("until") {
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
}
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.31") {

View File

@ -14,6 +14,7 @@ import (
"github.com/theupdateframework/notary"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
"vbom.ml/util/sortorder"
)
// trustTagKey represents a unique signed tag and hex-encoded hash pair
@ -28,26 +29,12 @@ type trustTagRow struct {
Signers []string
}
type trustTagRowList []trustTagRow
func (tagComparator trustTagRowList) Len() int {
return len(tagComparator)
}
func (tagComparator trustTagRowList) Less(i, j int) bool {
return tagComparator[i].SignedTag < tagComparator[j].SignedTag
}
func (tagComparator trustTagRowList) Swap(i, j int) {
tagComparator[i], tagComparator[j] = tagComparator[j], tagComparator[i]
}
// trustRepo represents consumable information about a trusted repository
type trustRepo struct {
Name string
SignedTags trustTagRowList
Signers []trustSigner
AdminstrativeKeys []trustSigner
Name string
SignedTags []trustTagRow
Signers []trustSigner
AdministrativeKeys []trustSigner
}
// trustSigner represents a trusted signer in a trusted repository
@ -64,20 +51,20 @@ type trustKey struct {
// lookupTrustInfo returns processed signature and role information about a notary repository.
// This information is to be pretty printed or serialized into a machine-readable format.
func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.RoleWithSignatures, []data.Role, error) {
func lookupTrustInfo(cli command.Cli, remote string) ([]trustTagRow, []client.RoleWithSignatures, []data.Role, error) {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), remote)
if err != nil {
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err
}
tag := imgRefAndAuth.Tag()
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPullOnly)
if err != nil {
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
if err = clearChangeList(notaryRepo); err != nil {
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, err
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, err
}
defer clearChangeList(notaryRepo)
@ -87,7 +74,7 @@ func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.
logrus.Debug(trust.NotaryError(remote, err))
// print an empty table if we don't have signed targets, but have an initialized notary repo
if _, ok := err.(client.ErrNoSuchTarget); !ok {
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote)
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signatures or cannot access %s", remote)
}
}
signatureRows := matchReleasedSignatures(allSignedTargets)
@ -95,7 +82,7 @@ func lookupTrustInfo(cli command.Cli, remote string) (trustTagRowList, []client.
// get the administrative roles
adminRolesWithSigs, err := notaryRepo.ListRoles()
if err != nil {
return trustTagRowList{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote)
return []trustTagRow{}, []client.RoleWithSignatures{}, []data.Role{}, fmt.Errorf("No signers for %s", remote)
}
// get delegation roles with the canonical key IDs
@ -138,8 +125,8 @@ func getDelegationRoleToKeyMap(rawDelegationRoles []data.Role) map[string][]stri
// aggregate all signers for a "released" hash+tagname pair. To be "released," the tag must have been
// signed into the "targets" or "targets/releases" role. Output is sorted by tag name
func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRowList {
signatureRows := trustTagRowList{}
func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagRow {
signatureRows := []trustTagRow{}
// do a first pass to get filter on tags signed into "targets" or "targets/releases"
releasedTargetRows := map[trustTagKey][]string{}
for _, tgt := range allTargets {
@ -162,6 +149,8 @@ func matchReleasedSignatures(allTargets []client.TargetSignedStruct) trustTagRow
for targetKey, signers := range releasedTargetRows {
signatureRows = append(signatureRows, trustTagRow{targetKey, signers})
}
sort.Sort(signatureRows)
sort.Slice(signatureRows, func(i, j int) bool {
return sortorder.NaturalLess(signatureRows[i].SignedTag, signatureRows[j].SignedTag)
})
return signatureRows
}

View File

@ -0,0 +1,33 @@
package trust
import (
"testing"
"github.com/docker/cli/cli/trust"
"github.com/theupdateframework/notary/client"
"github.com/theupdateframework/notary/tuf/data"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
)
func TestMatchReleasedSignaturesSortOrder(t *testing.T) {
var releasesRole = data.DelegationRole{BaseRole: data.BaseRole{Name: trust.ReleasesRole}}
targets := []client.TargetSignedStruct{
{Target: client.Target{Name: "target10-foo"}, Role: releasesRole},
{Target: client.Target{Name: "target1-foo"}, Role: releasesRole},
{Target: client.Target{Name: "target2-foo"}, Role: releasesRole},
}
rows := matchReleasedSignatures(targets)
var targetNames []string
for _, r := range rows {
targetNames = append(targetNames, r.SignedTag)
}
expected := []string{
"target1-foo",
"target2-foo",
"target10-foo",
}
assert.Check(t, is.DeepEqual(expected, targetNames))
}

View File

@ -107,9 +107,9 @@ func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) {
sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name })
return json.Marshal(trustRepo{
Name: remote,
SignedTags: signatureRows,
Signers: signerList,
AdminstrativeKeys: adminList,
Name: remote,
SignedTags: signatureRows,
Signers: signerList,
AdministrativeKeys: adminList,
})
}

View File

@ -8,6 +8,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/theupdateframework/notary/client"
"vbom.ml/util/sortorder"
)
func prettyPrintTrustInfo(cli command.Cli, remote string) error {
@ -51,7 +52,7 @@ func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures)
}
// pretty print with ordered rows
func printSignatures(out io.Writer, signatureRows trustTagRowList) error {
func printSignatures(out io.Writer, signatureRows []trustTagRow) error {
trustTagCtx := formatter.Context{
Output: out,
Format: formatter.NewTrustTagFormat(),
@ -78,13 +79,15 @@ func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error {
Format: formatter.NewSignerInfoFormat(),
Trunc: true,
}
formattedSignerInfo := formatter.SignerInfoList{}
formattedSignerInfo := []formatter.SignerInfo{}
for name, keyIDs := range roleToKeyIDs {
formattedSignerInfo = append(formattedSignerInfo, formatter.SignerInfo{
Name: name,
Keys: keyIDs,
})
}
sort.Sort(formattedSignerInfo)
sort.Slice(formattedSignerInfo, func(i, j int) bool {
return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name)
})
return formatter.SignerInfoWrite(signerInfoCtx, formattedSignerInfo)
}

View File

@ -1,6 +1,7 @@
package trust
import (
"bytes"
"encoding/hex"
"io/ioutil"
"testing"
@ -440,3 +441,20 @@ func TestFormatAdminRole(t *testing.T) {
targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil}
assert.Check(t, is.Equal("Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs)))
}
func TestPrintSignerInfoSortOrder(t *testing.T) {
roleToKeyIDs := map[string][]string{
"signer2-foo": {"B"},
"signer10-foo": {"C"},
"signer1-foo": {"A"},
}
expected := `SIGNER KEYS
signer1-foo A
signer2-foo B
signer10-foo C
`
buf := new(bytes.Buffer)
assert.NilError(t, printSignerInfo(buf, roleToKeyIDs))
assert.Check(t, is.Equal(expected, buf.String()))
}

View File

@ -4,8 +4,10 @@ import (
"io/ioutil"
"testing"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/theupdateframework/notary/client"
"gotest.tools/assert"
"gotest.tools/golden"
)
@ -41,91 +43,110 @@ func TestTrustInspectCommandErrors(t *testing.T) {
}
}
func TestTrustInspectCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
func TestTrustInspectCommandRepositoryErrors(t *testing.T) {
testCases := []struct {
doc string
args []string
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
err string
golden string
}{
{
doc: "OfflineErrors",
args: []string{"nonexistent-reg-name.io/image"},
notaryRepository: notary.GetOfflineNotaryRepository,
err: "No signatures or cannot access nonexistent-reg-name.io/image",
},
{
doc: "OfflineErrorsWithImageTag",
args: []string{"nonexistent-reg-name.io/image:tag"},
notaryRepository: notary.GetOfflineNotaryRepository,
err: "No signatures or cannot access nonexistent-reg-name.io/image:tag",
},
{
doc: "UninitializedErrors",
args: []string{"reg/unsigned-img"},
notaryRepository: notary.GetUninitializedNotaryRepository,
err: "No signatures or cannot access reg/unsigned-img",
golden: "trust-inspect-uninitialized.golden",
},
{
doc: "UninitializedErrorsWithImageTag",
args: []string{"reg/unsigned-img:tag"},
notaryRepository: notary.GetUninitializedNotaryRepository,
err: "No signatures or cannot access reg/unsigned-img:tag",
golden: "trust-inspect-uninitialized.golden",
},
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd = newInspectCommand(cli)
cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(tc.notaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.err)
if tc.golden != "" {
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
}
})
}
}
func TestTrustInspectCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
func TestTrustInspectCommand(t *testing.T) {
testCases := []struct {
doc string
args []string
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
golden string
}{
{
doc: "EmptyNotaryRepo",
args: []string{"reg/img:unsigned-tag"},
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
golden: "trust-inspect-empty-repo.golden",
},
{
doc: "FullRepoWithoutSigners",
args: []string{"signed-repo"},
notaryRepository: notary.GetLoadedWithNoSignersNotaryRepository,
golden: "trust-inspect-full-repo-no-signers.golden",
},
{
doc: "OneTagWithoutSigners",
args: []string{"signed-repo:green"},
notaryRepository: notary.GetLoadedWithNoSignersNotaryRepository,
golden: "trust-inspect-one-tag-no-signers.golden",
},
{
doc: "FullRepoWithSigners",
args: []string{"signed-repo"},
notaryRepository: notary.GetLoadedNotaryRepository,
golden: "trust-inspect-full-repo-with-signers.golden",
},
{
doc: "MultipleFullReposWithSigners",
args: []string{"signed-repo", "signed-repo"},
notaryRepository: notary.GetLoadedNotaryRepository,
golden: "trust-inspect-multiple-repos-with-signers.golden",
},
{
doc: "UnsignedTagInSignedRepo",
args: []string{"signed-repo:unsigned"},
notaryRepository: notary.GetLoadedNotaryRepository,
golden: "trust-inspect-unsigned-tag-with-signers.golden",
},
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd = newInspectCommand(cli)
cmd.SetArgs([]string{"reg/unsigned-img:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-uninitialized.golden")
}
func TestTrustInspectCommandEmptyNotaryRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"reg/img:unsigned-tag"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-empty-repo.golden")
}
func TestTrustInspectCommandFullRepoWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedWithNoSignersNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-no-signers.golden")
}
func TestTrustInspectCommandOneTagWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedWithNoSignersNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo:green"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-one-tag-no-signers.golden")
}
func TestTrustInspectCommandFullRepoWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-full-repo-with-signers.golden")
}
func TestTrustInspectCommandMultipleFullReposWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo", "signed-repo"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-multiple-repos-with-signers.golden")
}
func TestTrustInspectCommandUnsignedTagInSignedRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetLoadedNotaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs([]string{"signed-repo:unsigned"})
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-unsigned-tag-with-signers.golden")
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(tc.notaryRepository)
cmd := newInspectCommand(cli)
cmd.SetArgs(tc.args)
assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), tc.golden)
})
}
}

View File

@ -5,6 +5,7 @@ import (
"os"
"testing"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/notary"
"github.com/theupdateframework/notary/client"
@ -54,85 +55,92 @@ func TestTrustRevokeCommandErrors(t *testing.T) {
}
}
func TestTrustRevokeCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action."))
func TestTrustRevokeCommand(t *testing.T) {
testCases := []struct {
doc string
notaryRepository func(trust.ImageRefAndAuth, []string) (client.Repository, error)
args []string
expectedErr string
expectedMessage string
}{
{
doc: "OfflineErrors_Confirm",
notaryRepository: notary.GetOfflineNotaryRepository,
args: []string{"reg-name.io/image"},
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
},
{
doc: "OfflineErrors_Offline",
notaryRepository: notary.GetOfflineNotaryRepository,
args: []string{"reg-name.io/image", "-y"},
expectedErr: "could not remove signature for reg-name.io/image: client is offline",
},
{
doc: "OfflineErrors_WithTag_Offline",
notaryRepository: notary.GetOfflineNotaryRepository,
args: []string{"reg-name.io/image:tag"},
expectedErr: "could not remove signature for reg-name.io/image:tag: client is offline",
},
{
doc: "UninitializedErrors_Confirm",
notaryRepository: notary.GetUninitializedNotaryRepository,
args: []string{"reg-name.io/image"},
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
},
{
doc: "UninitializedErrors_NoTrustData",
notaryRepository: notary.GetUninitializedNotaryRepository,
args: []string{"reg-name.io/image", "-y"},
expectedErr: "could not remove signature for reg-name.io/image: does not have trust data for",
},
{
doc: "UninitializedErrors_WithTag_NoTrustData",
notaryRepository: notary.GetUninitializedNotaryRepository,
args: []string{"reg-name.io/image:tag"},
expectedErr: "could not remove signature for reg-name.io/image:tag: does not have trust data for",
},
{
doc: "EmptyNotaryRepo_Confirm",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"reg-name.io/image"},
expectedMessage: "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action.",
},
{
doc: "EmptyNotaryRepo_NoSignedTags",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"reg-name.io/image", "-y"},
expectedErr: "could not remove signature for reg-name.io/image: no signed tags to remove",
},
{
doc: "EmptyNotaryRepo_NoValidTrustData",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"reg-name.io/image:tag"},
expectedErr: "could not remove signature for reg-name.io/image:tag: No valid trust data for tag",
},
{
doc: "AllSigConfirmation",
notaryRepository: notary.GetEmptyTargetsNotaryRepository,
args: []string{"alpine"},
expectedMessage: "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action.",
},
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(tc.notaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
if tc.expectedErr != "" {
assert.ErrorContains(t, cmd.Execute(), tc.expectedErr)
return
}
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), tc.expectedMessage))
})
}
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetOfflineNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: client is offline")
}
func TestTrustRevokeCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action."))
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: does not have trust data for")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetUninitializedNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: does not have trust data for")
}
func TestTrustRevokeCommandEmptyNotaryRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image"})
cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for reg-name.io/image? [y/N] \nAborting action."))
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image", "-y"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image: no signed tags to remove")
cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd = newRevokeCommand(cli)
cmd.SetArgs([]string{"reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "could not remove signature for reg-name.io/image:tag: No valid trust data for tag")
}
func TestNewRevokeTrustAllSigConfirmation(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notary.GetEmptyTargetsNotaryRepository)
cmd := newRevokeCommand(cli)
cmd.SetArgs([]string{"alpine"})
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Please confirm you would like to delete all signature data for alpine? [y/N] \nAborting action."))
}
func TestGetSignableRolesForTargetAndRemoveError(t *testing.T) {

View File

@ -78,6 +78,7 @@ func isLastSignerForReleases(roleWithSig data.Role, allRoles []client.RoleWithSi
// removeSingleSigner attempts to remove a single signer and returns whether signer removal happened.
// The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation.
// nolint: unparam
func removeSingleSigner(cli command.Cli, repoName, signerName string, forceYes bool) (bool, error) {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), repoName)

View File

@ -3,7 +3,7 @@
"Name": "reg/img:unsigned-tag",
"SignedTags": [],
"Signers": [],
"AdminstrativeKeys": [
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [

View File

@ -11,7 +11,7 @@
}
],
"Signers": [],
"AdminstrativeKeys": [
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [

View File

@ -43,7 +43,7 @@
]
}
],
"AdminstrativeKeys": [
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [

View File

@ -43,7 +43,7 @@
]
}
],
"AdminstrativeKeys": [
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [
@ -106,7 +106,7 @@
]
}
],
"AdminstrativeKeys": [
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [

View File

@ -11,7 +11,7 @@
}
],
"Signers": [],
"AdminstrativeKeys": [
"AdministrativeKeys": [
{
"Name": "Root",
"Keys": [

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