Compare commits

...
94 Commits
Author SHA1 Message Date
Silvin Lubecki 51668a30f2 Merge pull request #1483 from thaJeztah/18.09_backport_docs_fixes
[18.09 backport] various docs fixes
2018-10-26 15:54:26 +01:00
Akihiro Suda 5e7f9d3c84 docs, ssh: unsupport password auth explicitly
The issue with password auth is tracked in #1476 and #1477 .

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
(cherry picked from commit 16b014e062)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:33 +02:00
Brandon Mitchell 72ddefbada Documenting example default-ulimit in daemon.json
Signed-off-by: Brandon Mitchell <git@bmitch.net>
(cherry picked from commit 3f4f450941)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:31 +02:00
Brandon Mitchell 135aa72476 Updating rmi doc example to specify latest tag
Signed-off-by: Brandon Mitchell <git@bmitch.net>
(cherry picked from commit f913b73c81)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:28 +02:00
Scott Brenner 7c7fe26a6f Minor typo fix in run documentation
Quick syntax fix!

Signed-off-by: Scott Brenner <scott@scottbrenner.me>
(cherry picked from commit 50143cff12)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:26 +02:00
Jake Lambert 1df47ffb4d Clarify in docs that docker tag doesn't publish
I am attempting to push a tag to a private repository. The documentation for `docker tag`  has an explicit example to for how ["To push an image to a private registry"](https://docs.docker.com/engine/reference/commandline/tag/#tag-an-image-referenced-by-name). My colleague clarified that this command does not in fact push anything, so I thought this PR might save some future novice the same confusion.

Signed-off-by: Jake Lambert <jake.lambert@volusion.com>
(cherry picked from commit 4ed484bac4)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:24 +02:00
Thomas Riccardi 2e7e529a18 Fix login documentation layout
ddadd3db49 mass standardized the
formatting, with some errors.

This commit fixes errors on `login.md`:
- revert wrong `Logging out` headline
- restore correct level for some headlines (relative to parent
  headline level change)
- re-add `Usage` headlines, with better name

Also add `related commands` headline on `login` and `logout`.

Signed-off-by: Thomas Riccardi <thomas@deepomatic.com>
(cherry picked from commit a0e3ec8790)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:22 +02:00
Alex Mayer f8f230181e Docs: Add Spaces Around Parenthesis Where Needed
Signed-off-by: Alex Mayer <amayer5125@gmail.com>
(cherry picked from commit 2b0fdd0f17)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:19 +02:00
Lihua Tang 0ee4693953 Typo fix
Signed-off-by: Lihua Tang <lhtang@alauda.io>
(cherry picked from commit ca5e453180)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:17 +02:00
Rui Cao cb4cd04c64 Typo fix: filesytem -> filesystem
Signed-off-by: Rui Cao <ruicao@alauda.io>
(cherry picked from commit 2eb95909ee)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:15 +02:00
Anda Xu d2e771fed6 update docs with current supported features options
Signed-off-by: Anda Xu <anda.xu@docker.com>
(cherry picked from commit d656706678)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-25 15:30:09 +02:00
Vincent Demeester b8911a3b33 Merge pull request #1481 from thaJeztah/18.09_backport_fix_docker_invalid_subcommand
[18.09 backport] Fix `docker invalid-subcommand` regression
2018-10-25 09:35:28 +02:00
Akihiro Suda ebe071a9b3 Fix docker invalid-subcommand regression
Starting with a3fe7d62b8,
`docker invalid-subcommand` did not exit with non-zero status.

Fix #1428

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
(cherry picked from commit d708cada43)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-24 20:49:10 +02:00
Andrew Hsu ecb972ab38 Merge pull request #1475 from tiborvass/18.09-fix-build-stream
[18.09] build: update session support detection
2018-10-22 19:24:24 -07:00
Tibor Vass 4c68a9666f build: update session support detection
Avoid testing for session support in non-buildkit builder to support
servers that falsely report as `1.39` compatible

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit 3e8c41beb0)
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-10-22 23:02:08 +00:00
Andrew Hsu e245b72381 Merge pull request #1458 from dhiltgen/bump_licensing_lib
[18.09] Bump licensing lib
2018-10-19 13:58:37 -07:00
Daniel Hiltgen 0ff9e5cd10 Remove e-mail from trial flow
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-10-18 14:56:59 -07:00
Daniel Hiltgen 8e565d0399 Bump licensing library
Removes the billing profile flow which is now handled on the back-end.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-10-18 14:44:05 -07:00
Sebastiaan van Stijn 8a424333f9 Merge pull request #1455 from thaJeztah/18.09_backport_legacy_drivers
[18.09 backport] deprecate devicemapper and legacy overlay storage drivers
2018-10-18 00:46:50 +02:00
Sebastiaan van Stijn fde819236b Deprecate "devicemapper" storage driver.
The `devicemapper` storage driver is deprecated in favor of `overlay2`, and will
be removed in a future release. Users of the `devicemapper` storage driver are
recommended to migrate to a different storage driver, such as `overlay2`, which
is now the default storage driver.

The `devicemapper` storage driver facilitates running Docker on older (3.x) kernels
that have no support for other storage drivers (such as overlay2, or AUFS).

Now that support for `overlay2` is added to all supported distros (as they are
either on kernel 4.x, or have support for multiple lowerdirs backported), there
is no reason to continue maintenance of the `devicemapper` storage driver.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 662441ba31)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-17 18:04:04 +02:00
Sebastiaan van Stijn aa6314c663 Deprecate legacy overlay storage driver
The `overlay` storage driver is deprecated in favor of the `overlay2` storage
driver, which has all the benefits of `overlay`, without its limitations (excessive
inode consumption). The legacy `overlay` storage driver will be removed in a future
release. Users of the `overlay` storage driver should migrate to the `overlay2`
storage driver.

The legacy `overlay` storage driver allowed using overlayFS-backed filesystems
on pre 4.x kernels. Now that all supported distributions are able to run `overlay2`
(as they are either on kernel 4.x, or have support for multiple lowerdirs
backported), there is no reason to keep maintaining the `overlay` storage driver.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8bc2aa45a6)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-17 18:03:57 +02:00
Sebastiaan van Stijn 81ee98e861 Merge pull request #1453 from tiborvass/18.09-builder-prune-filter-unused-for
[18.09 backport] builder/prune: rename max-age filter to unused-for in help output
2018-10-17 17:56:13 +02:00
Brian Goff 22336b332c Merge pull request #1432 from thaJeztah/18.09_backport_use_string_builder
[18.09] backport using strings.Builder instead of string appending
2018-10-16 19:42:29 -07:00
Tibor Vass 2961611fda builder/prune: rename max-age filter to unused-for in help output
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit c9ce6dc656)
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-10-17 00:37:24 +00:00
Sebastiaan van Stijn 17adf05188 Merge pull request #1421 from dhiltgen/final_url
[18.09] Update release note link to final location
2018-10-16 19:42:48 +02:00
Tibor Vass 39f1110308 Merge pull request #1438 from thaJeztah/18.09_backport_hide-buildkit-flags-if-not-enabled
[18.09 backport] builder / buildkit updates
2018-10-11 15:35:44 -07:00
Tibor Vass 3dfacb55a4 build: only show buildkit-specific flags if buildkit is enabled
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit bbd01fe3df)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-12 00:16:48 +02:00
Akihiro Suda e942084530 build: add SSH agent socket forwarder (docker build --ssh $SSHMOUNTID=$SSH_AUTH_SOCK)
Unlike `docker build --secret`, `docker build --ssh` allows the build container to
use SSH keys with passphrases.

  $ eval $(ssh-agent)
  $ ssh-add ~/.ssh/id_rsa
  (Input your passphrase here)
  $ docker build --ssh default=$SSH_AUTH_SOCK ...

This feature requires the daemon with `CapExecMountSSH` build capability (moby/moby#37973) .

Currently, the official Dockerfile frontend does not provide the syntax for using the SSH forwarder.

However, the experimental `RUN --mount=type=ssh` syntax can be enabled by using
the Dockerfile frontend image built with the `BUILDTAGS="dfrunmount dfssh"`, via the `# syntax =` "shebang".

The Dockerfile for the Dockerfile frontend is available at  github.com/moby/buildkit/frontend/dockerfile/cmd/dockerfile-frontend)
The pre-built image is also available as `tonistiigi/dockerfile:ssh20181002` .

An example Dockerfile with `RUN --mount=type=ssh`:

  # syntax = tonistiigi/dockerfile:ssh20181002
  FROM alpine
  RUN apk add --no-cache openssh-client
  RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
  RUN --mount=type=ssh ssh git@gitlab.com | tee /hello
  # "Welcome to GitLab, @GITLAB_USERNAME_ASSOCIATED_WITH_SSHKEY" should be printed here

More info available at moby/buildkit#608, moby/buildkit#655

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
(cherry picked from commit db7399a016)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-12 00:16:42 +02:00
Akihiro Suda 50f529fa47 bump up buildkit
Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
(cherry picked from commit 846c38cbd7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-12 00:16:38 +02:00
Sebastiaan van Stijn b4bee9be75 Bump buildkit and dependencies to 39404586a50d1b9d0fb1c578cf0f4de7bdb7afe5
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 8cfd24049f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-12 00:16:29 +02:00
Li Yi 8b0d34a5a1 Using strings.Builder instead of string appending
Signed-off-by: Li Yi <denverdino@gmail.com>
(cherry picked from commit 814ced4b30)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-10-10 20:21:52 +02:00
Daniel Hiltgen f93908213a Update release note link to final location
We'll be using a redirect from this URL to the back-end docs system for
hosting release notes. Final location confirmed with Docs team and PM.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-10-08 16:25:49 -07:00
Sebastiaan van Stijn 4280972d65 Merge pull request #1402 from AkihiroSuda/fix-kill-warning-1809
[18.09] backport connhelper: try sending SIGTERM before SIGKILL
2018-10-04 16:53:57 +02:00
Andrew Hsu 984bc7411e Merge pull request #1408 from dhiltgen/fix_panic
[18.09] Fix panic in display only case for license
2018-10-02 15:41:22 -07:00
Daniel Hiltgen 92932647d3 Add test coverage for display only with hub licenses
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-10-02 11:21:22 -07:00
Daniel Hiltgen dee37936e5 Fix panic in display only case for license
Prior refactoring passes missed a corner case.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-10-02 10:24:16 -07:00
Silvin Lubecki 3e1a0bdc23 Merge pull request #1406 from dhiltgen/revamp_18.09
Remove unused helath check func
2018-10-02 11:26:36 +02:00
Daniel Hiltgen f2b2061cc3 Remove unused helath check func
During the refactoring for 18.09 the activate/update flows no longer
restart the engine explicitly but let the user do that when they're ready,
so the health check logic is no longer required.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-10-01 15:12:09 -07:00
Akihiro Suda 4925fd9c34 connhelper: try sending SIGTERM before SIGKILL
Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
(cherry picked from commit acbb0eb6da)
Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
2018-09-30 10:01:20 +09:00
Andrew Hsu 5d3ab5bc0c Merge pull request #1388 from dhiltgen/revamp_18.09
[18.09] Refine how metadata dir is handled
2018-09-28 14:23:12 -07:00
Daniel Hiltgen c12e23a4c1 Refine how metadata dir is handled
This is a follow up PR to #1381 to address some of the review comments
we didn't get to.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-28 14:14:13 -07:00
Andrew Hsu aca3f2d382 Merge pull request #1387 from dhiltgen/activate_ux
[18.09] Expose licensing details before loading
2018-09-28 14:02:48 -07:00
Andrew Hsu a7488d1bcd use gotest.tools/fs for TestActivateExpiredLicenseDryRun
Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-09-28 20:50:43 +00:00
Daniel Hiltgen 5a97a93ae1 Expose licensing details before loading
Help the user understand which license they're about
to load in case they have multiple licenses they need to
figure out.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-28 20:50:43 +00:00
Daniel Hiltgen 41910b6d68 Vendor bump of licensing lib
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-28 20:50:43 +00:00
Andrew Hsu 1a087e87c9 Merge pull request #1389 from mason-fish/fix-subscription-filter
[18.09] fix subscription filter
2018-09-26 16:12:41 -07:00
Sebastiaan van Stijn 0b11120060 Merge pull request #1394 from thaJeztah/18.09_backport_
[18.09] backport fix substitution with non-empty env-var
2018-09-26 15:33:01 +02:00
Silvin Lubecki e57b20642d Merge pull request #1342 from tonistiigi/1809-fix-os-race
[18.09] backport connhelper: fix cmd.Wait() race
2018-09-26 15:30:00 +02:00
Sebastiaan van Stijn b8702b8a9a Fix substitution with non-empty env-var
Due to a typo, substitution would not work if the given
environment-variable was set.

Given the following docker compose file;

```yaml
version: "3.7"

services:
  app:
    image: nginx:${version:-latest}
```

Deploying a stack with `$version` set would ignore the `$version`
environment variable, and use the default value instead;

```bash
version=alpine docker stack deploy -c docker-compose.yml foobar

Creating network foobar_default
Creating service foobar_app

docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
rskkjxe6sm0w        foobar_app          replicated          1/1                 nginx:latest
```

This patch also fixes "soft default" not detecting empty environment variables,
only non-set environment variables.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit ec3daea021)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-26 15:20:15 +02:00
Mason Fish a31b20d7db fix subscription filter
Signed-off-by: Mason Fish <mason.fish@docker.com>
2018-09-24 16:56:05 -07:00
Andrew Hsu 5ba5678898 Merge pull request #1386 from tiborvass/18.09-df-verbose-format-raw
[18.09] system/df: allow -v with --format
2018-09-22 00:25:21 -07:00
Tibor Vass 9de1318e36 system/df: allow -v with --format
This allows to provide more information for build cache disk usage.

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit a90b99edfc)
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-09-22 01:33:03 +00:00
Andrew Hsu 19e1ab273e Merge pull request #1381 from dhiltgen/revamp_18.09
[18.09] `docker engine` updates
2018-09-21 15:12:54 -07:00
Daniel Hiltgen ec1812188f Fix vendoring glitch
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-21 13:16:34 -07:00
Daniel Hiltgen 6004d74b1f Fix lint glitches
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-21 11:01:17 -07:00
Sebastiaan van Stijn e79e591ee9 Merge pull request #1382 from thaJeztah/18.09_manifest-inspect-insecure-fix
[18.09] backport fix insecure manifest inspect with restrictive certs perms
2018-09-21 12:53:24 +02:00
Daniel Hiltgen 0f22d7e295 Remove metadata file before writing
The packages will deliver this as a link so lets make sure we don't
write through the link to the underlying packaged file.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-20 18:00:46 -07:00
Daniel Hiltgen f250152bf4 Review comments
Address code review comemnts and purge additional dead code.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-20 12:01:20 -07:00
Christy Norman f9d666b057 fix insecure manifest inspect with restrictive certs perms
If, for some reason, the certs directory has permissions that are
inaccessible by docker, we should still be able to fetch manifests using
the `insecure` flag.

Since the cli doesn't access the engine's list of insecure registries,
the registry client should make a singleton list of the registry being queried with the
`insecure` flag.

Closes #1358

Signed-off-by: Christy Norman <christy@linux.vnet.ibm.com>
(cherry picked from commit d57adbc034)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-20 19:53:10 +02:00
Daniel Hiltgen 342afe44fb Refined engine implementations
Adapt the CLI to the host install model for 18.09.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-09-19 20:10:31 -07:00
Michael Crosby cfec8027ed Install binaries on host for upgrade
Signed-off-by: Michael Crosby <crosbymichael@gmail.com>
2018-09-19 19:06:28 -07:00
Silvin Lubecki 78c42cf031 Merge pull request #1375 from thaJeztah/18.09_backport_importlcow
[18.09] backport LCOW: --platform on import (already in API)
2018-09-14 15:06:21 +02:00
John Howard dd2f13bed4 LCOW: --platform on import (already in API)
Signed-off-by: John Howard <jhoward@microsoft.com>
(cherry picked from commit b55a0b681f)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-14 14:26:39 +02:00
Silvin Lubecki 3b991ec615 Merge pull request #1373 from thaJeztah/18.09_backport_move_test_function_in_there
[18.09] backport getEngineConfigFilePath is only used during test so moving it in test files for now.
2018-09-14 13:20:12 +02:00
Sebastiaan van Stijn 34ea8bb5a5 Merge pull request #1374 from thaJeztah/18.09_backport_docs_fixes
[18.09] backport update usage for 'docker build' with '--progress' and '--secret' options
2018-09-14 12:55:50 +02:00
Anda Xu afb17ec70b update usage for 'docker build' with '--progress' and '--secret' options
Signed-off-by: Anda Xu <anda.xu@docker.com>
(cherry picked from commit 83ca55db7d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-14 12:35:22 +02:00
Vincent Demeester 62aed95bc1 getEngineConfigFilePath is only used during test…
… so moving it in test files for now.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit 37ca5d6813)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-14 12:27:13 +02:00
Silvin Lubecki 649e4916bb Merge pull request #1368 from thaJeztah/carry-1360-store_prefix
[18.09] Prefix engine repo with store
2018-09-13 08:24:45 +02:00
Daniel Hiltgen 3597d75281 Prefix engine repo with store
The official access point for the Q3 engine images will
be prefixed by store.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-12 13:43:04 +02:00
Vincent Demeester 5673816fec Merge pull request #1364 from thaJeztah/18.09-backport_contains-containerized
[18.09] backport: remove containerizedengine package dependency from docker/cli/command…
2018-09-12 08:55:12 +02:00
Silvin Lubecki a8c69c8287 Merge pull request #1365 from thaJeztah/18.09-backport_engine-only-linux
[18.09] backport add `docker engine` commands only on Linux…
2018-09-11 18:04:57 +02:00
Vincent Demeester fc3dc8f058 Remove containerizedengine package dependency from docker/cli/command…
… this removes a whole lot of dependencies from people depending on docker/cli…

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit 2d344b2f61)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-11 17:01:02 +02:00
Vincent Demeester 2a46a3d46c Add docker engine commands only on Linux…
… this is, for now, the only platform that is supported

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
(cherry picked from commit a3a955f204)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-09-11 16:57:54 +02:00
Akihiro Suda b2cf18ac2e connhelper: fix cmd.Wait() race
Fix #1336

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
(cherry picked from commit a22853e64d)
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2018-09-05 16:03:41 -07:00
Andrew Hsu 44371c7c34 Merge pull request #1339 from andrewhsu/vr
[18.09] vndr docker/docker to docker/engine d2ecc7b
2018-09-05 11:43:09 -07:00
Andrew Hsu 4e6798794d vndr docker/docker to docker/engine d2ecc7b
And update the associated packages that have also updated from
docker/docker vendor.conf.

Signed-off-by: Andrew Hsu <andrewhsu@docker.com>
2018-09-05 17:35:51 +00:00
Tibor Vass d8aefad94a Merge pull request #1334 from tiborvass/18.09-cmd-builder-prune-with-options
[18.09] build: add options to builder prune
2018-09-04 22:35:23 -07:00
Tibor Vass 3c37d6a034 system df: show table output for build cache
Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit ca608c2302)
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-09-05 03:33:30 +00:00
Tibor Vass 9d43f1ed48 build: add options to builder prune
This patch adds --filter, --keep-storage, --all and --force to builder prune.

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit c806eb49c9)
Signed-off-by: Tibor Vass <tibor@docker.com>
2018-09-05 03:33:30 +00:00
Vincent Demeester a818677813 Merge pull request #1322 from thaJeztah/18.09_backport_bump_kube_deps
[18.09] backport: bump kube dependency to 1.11.2
2018-08-29 17:56:26 +02:00
Vincent Demeester c204959687 Merge pull request #1323 from thaJeztah/18.09_backport_bump_golang_1.10.4
[18.09] backport: bump Go to 1.10.4
2018-08-29 15:32:30 +02:00
Sebastiaan van Stijn 76c09259db Bump Go to 1.10.4
Includes fixes to the go command, linker, and the net/http, mime/multipart,
ld/macho, bytes, and strings packages. See the Go 1.10.4 milestone on the
issue tracker for details:

https://github.com/golang/go/issues?q=milestone%3AGo1.10.4

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 44ca0901d1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-29 15:19:34 +02:00
Marcus Martins 0efb62cab1 Bump kube dependency to 1.11.2
Bump our kube dependencies to the latest patch
level for kube 1.11.

Signed-off-by: Marcus Martins <marcus@docker.com>
(cherry picked from commit c67e05796b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-29 14:59:20 +02:00
Andrew Hsu 8789e93d6e Merge pull request #1313 from dhiltgen/product_license
[18.09] Expose product license in info output
2018-08-28 21:05:09 -07:00
Andrew Hsu 0ee05a6353 Merge pull request #1312 from dhiltgen/dual_keys
[18.09] Add support for multiple signing keys
2018-08-28 20:51:22 -07:00
Daniel Hiltgen 68be7cb376 Expose product license in info output
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-28 20:10:09 -07:00
Andrew Hsu de805da04c Merge pull request #1317 from dhiltgen/play_nice_with_proxy
Update proxy config if present
2018-08-28 15:01:56 -07:00
Andrew Hsu b75350de7a Merge pull request #1311 from dhiltgen/fix_progress
[18.09] Fix progress reporting for containerd pulls
2018-08-28 14:59:59 -07:00
Daniel Hiltgen f96ddaedf7 Update proxy config if present
If the system has a containerd-proxy managing the lifecycle
of the daemon, make sure the config is updated with the new image
on update.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-28 10:11:44 -07:00
Daniel Hiltgen 0fb6bb35a4 Add support for multiple signing keys
Basic enterprise licenses and platform license keys will be signed with
two different keys in the upcoming release.  This adds support for the
CLI to support both variants.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-27 15:20:30 -07:00
Daniel Hiltgen 264ee43c2a Vendor bump for licensing library to support multiple signing keys
Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-27 15:20:19 -07:00
Daniel Hiltgen 7f4c842e8a Fix progress reporting for containerd pulls
During refactoring of the implementation PR progress reporting
was broken.  This gets the progress reporting back in action.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
2018-08-27 14:44:13 -07:00
Sebastiaan van Stijn e25e9d68be Merge pull request #1310 from thaJeztah/18.09-backport_update-docs
[18.09] backport: update docs with the new features option in daemon.json
2018-08-27 13:46:15 +02:00
Anda Xu 6877dedeee update docs with the new features option in daemon.json
Signed-off-by: Anda Xu <anda.xu@docker.com>
(cherry picked from commit 3e0b0a6692)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2018-08-27 11:51:39 +02:00
341 changed files with 29421 additions and 11777 deletions
+2 -2
View File
@@ -12,14 +12,14 @@ clean: ## remove build artifacts
.PHONY: test-unit .PHONY: test-unit
test-unit: ## run unit test test-unit: ## run unit test
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/') ./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: test .PHONY: test
test: test-unit ## run tests test: test-unit ## run tests
.PHONY: test-coverage .PHONY: test-coverage
test-coverage: ## run test coverage test-coverage: ## run test coverage
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/') ./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
.PHONY: lint .PHONY: lint
lint: ## run all the lint tools lint: ## run all the lint tools
+1 -1
View File
@@ -4,7 +4,7 @@ clone_folder: c:\gopath\src\github.com\docker\cli
environment: environment:
GOPATH: c:\gopath GOPATH: c:\gopath
GOVERSION: 1.10.3 GOVERSION: 1.10.4
DEPVERSION: v0.4.1 DEPVERSION: v0.4.1
install: install:
+67 -2
View File
@@ -3,29 +3,94 @@ package builder
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
units "github.com/docker/go-units" units "github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type pruneOptions struct {
force bool
all bool
filter opts.FilterOpt
keepStorage opts.MemBytes
}
// NewPruneCommand returns a new cobra prune command for images // NewPruneCommand returns a new cobra prune command for images
func NewPruneCommand(dockerCli command.Cli) *cobra.Command { func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
options := pruneOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "prune", Use: "prune",
Short: "Remove build cache", Short: "Remove build cache",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
report, err := dockerCli.Client().BuildCachePrune(context.Background()) spaceReclaimed, output, err := runPrune(dockerCli, options)
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(report.SpaceReclaimed))) if output != "" {
fmt.Fprintln(dockerCli.Out(), output)
}
fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil return nil
}, },
Annotations: map[string]string{"version": "1.39"}, Annotations: map[string]string{"version": "1.39"},
} }
flags := cmd.Flags()
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused images, not just dangling ones")
flags.Var(&options.filter, "filter", "Provide filter values (e.g. 'unused-for=24h')")
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
return cmd return cmd
} }
const (
normalWarning = `WARNING! This will remove all dangling build cache. Are you sure you want to continue?`
allCacheWarning = `WARNING! This will remove all build cache. Are you sure you want to continue?`
)
func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
warning := normalWarning
if options.all {
warning = allCacheWarning
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
return 0, "", nil
}
report, err := dockerCli.Client().BuildCachePrune(context.Background(), types.BuildCachePruneOptions{
All: options.all,
KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters,
})
if err != nil {
return 0, "", err
}
if len(report.CachesDeleted) > 0 {
var sb strings.Builder
sb.WriteString("Deleted build cache objects:\n")
for _, id := range report.CachesDeleted {
sb.WriteString(id)
sb.WriteByte('\n')
}
output = sb.String()
}
return report.SpaceReclaimed, output, nil
}
// CachePrune executes a prune command for build cache
func CachePrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
}
+30 -14
View File
@@ -8,6 +8,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"time" "time"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
@@ -19,8 +20,8 @@ import (
manifeststore "github.com/docker/cli/cli/manifest/store" manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client" registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/containerizedengine"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api" "github.com/docker/docker/api"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
@@ -55,20 +56,21 @@ type Cli interface {
ManifestStore() manifeststore.Store ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool ContentTrustEnabled() bool
NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
// Instances of the client can be returned from NewDockerCli. // Instances of the client can be returned from NewDockerCli.
type DockerCli struct { type DockerCli struct {
configFile *configfile.ConfigFile configFile *configfile.ConfigFile
in *InStream in *InStream
out *OutStream out *OutStream
err io.Writer err io.Writer
client client.APIClient client client.APIClient
serverInfo ServerInfo serverInfo ServerInfo
clientInfo ClientInfo clientInfo ClientInfo
contentTrust bool contentTrust bool
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
} }
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. // DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
@@ -132,6 +134,20 @@ func (cli *DockerCli) ContentTrustEnabled() bool {
return cli.contentTrust return cli.contentTrust
} }
// BuildKitEnabled returns whether buildkit is enabled either through a daemon setting
// or otherwise the client-side DOCKER_BUILDKIT environment variable
func BuildKitEnabled(si ServerInfo) (bool, error) {
buildkitEnabled := si.BuildkitVersion == types.BuilderBuildKit
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" {
var err error
buildkitEnabled, err = strconv.ParseBool(buildkitEnv)
if err != nil {
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
}
}
return buildkitEnabled, nil
}
// ManifestStore returns a store for local manifests // ManifestStore returns a store for local manifests
func (cli *DockerCli) ManifestStore() manifeststore.Store { func (cli *DockerCli) ManifestStore() manifeststore.Store {
// TODO: support override default location from config file // TODO: support override default location from config file
@@ -233,8 +249,8 @@ func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions
} }
// NewContainerizedEngineClient returns a containerized engine client // NewContainerizedEngineClient returns a containerized engine client
func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) { func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) {
return containerizedengine.NewClient(sockPath) return cli.newContainerizeClient(sockPath)
} }
// ServerInfo stores details about the supported features and platform of the // ServerInfo stores details about the supported features and platform of the
@@ -252,8 +268,8 @@ type ClientInfo struct {
} }
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerCli { func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli {
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted} return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
} }
// NewAPIClientFromFlags creates a new APIClient from command line flags // NewAPIClientFromFlags creates a new APIClient from command line flags
+5 -4
View File
@@ -2,6 +2,7 @@ package commands
import ( import (
"os" "os"
"runtime"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/builder" "github.com/docker/cli/cli/command/builder"
@@ -85,9 +86,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
// volume // volume
volume.NewVolumeCommand(dockerCli), volume.NewVolumeCommand(dockerCli),
// engine
engine.NewEngineCommand(dockerCli),
// legacy commands may be hidden // legacy commands may be hidden
hide(system.NewEventsCommand(dockerCli)), hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInfoCommand(dockerCli)), hide(system.NewInfoCommand(dockerCli)),
@@ -124,7 +122,10 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
hide(image.NewSaveCommand(dockerCli)), hide(image.NewSaveCommand(dockerCli)),
hide(image.NewTagCommand(dockerCli)), hide(image.NewTagCommand(dockerCli)),
) )
if runtime.GOOS == "linux" {
// engine
cmd.AddCommand(engine.NewEngineCommand(dockerCli))
}
} }
func hide(cmd *cobra.Command) *cobra.Command { func hide(cmd *cobra.Command) *cobra.Command {
+1 -1
View File
@@ -73,6 +73,6 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
// RunPrune calls the Container Prune API // RunPrune calls the Container Prune API
// This returns the amount of space reclaimed and a detailed output string // This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) { func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
} }
+49 -21
View File
@@ -3,11 +3,12 @@ package engine
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/cli/internal/licenseutils" "github.com/docker/cli/internal/licenseutils"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/licensing/model" "github.com/docker/licensing/model"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -15,19 +16,21 @@ import (
) )
type activateOptions struct { type activateOptions struct {
licenseFile string licenseFile string
version string version string
registryPrefix string registryPrefix string
format string format string
image string image string
quiet bool quiet bool
displayOnly bool displayOnly bool
sockPath string sockPath string
licenseLoginFunc func(ctx context.Context, authConfig *types.AuthConfig) (licenseutils.HubUser, error)
} }
// newActivateCommand creates a new `docker engine activate` command // newActivateCommand creates a new `docker engine activate` command
func newActivateCommand(dockerCli command.Cli) *cobra.Command { func newActivateCommand(dockerCli command.Cli) *cobra.Command {
var options activateOptions var options activateOptions
options.licenseLoginFunc = licenseutils.Login
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "activate [OPTIONS]", Use: "activate [OPTIONS]",
@@ -56,10 +59,10 @@ https://hub.docker.com/ then specify the file with the '--license' flag.
flags.StringVar(&options.licenseFile, "license", "", "License File") 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.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.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the default location where engine images are pulled")
flags.StringVar(&options.image, "engine-image", containerizedengine.EnterpriseEngineImage, "Specify engine image") flags.StringVar(&options.image, "engine-image", "", "Specify engine image")
flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template") 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.displayOnly, "display-only", false, "only display license information and exit")
flags.BoolVar(&options.quiet, "quiet", false, "Only display available licenses by ID") flags.BoolVar(&options.quiet, "quiet", false, "Only display available licenses by ID")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint") flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
@@ -67,6 +70,9 @@ https://hub.docker.com/ then specify the file with the '--license' flag.
} }
func runActivate(cli command.Cli, options activateOptions) error { func runActivate(cli command.Cli, options activateOptions) error {
if !isRoot() {
return errors.New("this command must be run as a privileged user")
}
ctx := context.Background() ctx := context.Background()
client, err := cli.NewContainerizedEngineClient(options.sockPath) client, err := cli.NewContainerizedEngineClient(options.sockPath)
if err != nil { if err != nil {
@@ -94,26 +100,48 @@ func runActivate(cli command.Cli, options activateOptions) error {
return err return err
} }
} }
if err = licenseutils.ApplyLicense(ctx, cli.Client(), license); err != nil { summary, err := licenseutils.GetLicenseSummary(ctx, *license)
if err != nil {
return err
}
fmt.Fprintf(cli.Out(), "License: %s\n", summary)
if options.displayOnly {
return nil
}
dclient := cli.Client()
if err = licenseutils.ApplyLicense(ctx, dclient, license); err != nil {
return err return err
} }
opts := containerizedengine.EngineInitOptions{ // Short circuit if the user didn't specify a version and we're already running enterprise
if options.version == "" {
serverVersion, err := dclient.ServerVersion(ctx)
if err != nil {
return err
}
if strings.Contains(strings.ToLower(serverVersion.Platform.Name), "enterprise") {
fmt.Fprintln(cli.Out(), "Successfully activated engine license on existing enterprise engine.")
return nil
}
options.version = serverVersion.Version
}
opts := clitypes.EngineInitOptions{
RegistryPrefix: options.registryPrefix, RegistryPrefix: options.registryPrefix,
EngineImage: options.image, EngineImage: options.image,
EngineVersion: options.version, EngineVersion: options.version,
} }
return client.ActivateEngine(ctx, opts, cli.Out(), authConfig, if err := client.ActivateEngine(ctx, opts, cli.Out(), authConfig); err != nil {
func(ctx context.Context) error { return err
client := cli.Client() }
_, err := client.Ping(ctx) fmt.Fprintln(cli.Out(), `Successfully activated engine.
return err Restart docker with 'systemctl restart docker' to complete the activation.`)
}) return nil
} }
func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) { func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) {
user, err := licenseutils.Login(ctx, authConfig) user, err := options.licenseLoginFunc(ctx, authConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+112 -3
View File
@@ -1,19 +1,35 @@
package engine package engine
import ( import (
"context"
"fmt" "fmt"
"testing" "testing"
"time"
"github.com/docker/cli/internal/containerizedengine" "github.com/docker/cli/internal/licenseutils"
"github.com/docker/cli/internal/test"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/docker/licensing"
"github.com/docker/licensing/model"
"gotest.tools/assert" "gotest.tools/assert"
"gotest.tools/fs"
"gotest.tools/golden"
)
const (
// nolint: lll
expiredLicense = `{"key_id":"irlYm3b9fdD8hMUXjazF39im7VQSSbAm9tfHK8cKUxJt","private_key":"aH5tTRDAVJpCRS2CRetTQVXIKgWUPfoCHODhDvNPvAbz","authorization":"ewogICAicGF5bG9hZCI6ICJleUpsZUhCcGNtRjBhVzl1SWpvaU1qQXhPQzB3TXkweE9GUXdOem93TURvd01Gb2lMQ0owYjJ0bGJpSTZJbkZtTVMxMlVtRmtialp5YjFaMldXdHJlVXN4VFdKMGNGUmpXR1ozVjA4MVRWZFFTM2cwUnpJd2NIYzlJaXdpYldGNFJXNW5hVzVsY3lJNk1Td2ljMk5oYm01cGJtZEZibUZpYkdWa0lqcDBjblZsTENKc2FXTmxibk5sVkhsd1pTSTZJazltWm14cGJtVWlMQ0owYVdWeUlqb2lVSEp2WkhWamRHbHZiaUo5IiwKICAgInNpZ25hdHVyZXMiOiBbCiAgICAgIHsKICAgICAgICAgImhlYWRlciI6IHsKICAgICAgICAgICAgImp3ayI6IHsKICAgICAgICAgICAgICAgImUiOiAiQVFBQiIsCiAgICAgICAgICAgICAgICJrZXlJRCI6ICJKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVCIsCiAgICAgICAgICAgICAgICJraWQiOiAiSjdMRDo2N1ZSOkw1SFo6VTdCQToyTzRHOjRBTDM6T0YyTjpKSEdCOkVGVEg6NUNWUTpNRkVPOkFFSVQiLAogICAgICAgICAgICAgICAia3R5IjogIlJTQSIsCiAgICAgICAgICAgICAgICJuIjogInlkSXktbFU3bzdQY2VZLTQtcy1DUTVPRWdDeUY4Q3hJY1FJV3VLODRwSWlaY2lZNjczMHlDWW53TFNLVGx3LVU2VUNfUVJlV1Jpb01OTkU1RHM1VFlFWGJHRzZvbG0ycWRXYkJ3Y0NnLTJVVUhfT2NCOVd1UDZnUlBIcE1GTXN4RHpXd3ZheThKVXVIZ1lVTFVwbTFJdi1tcTdscDVuUV9SeHJUMEtaUkFRVFlMRU1FZkd3bTNoTU9fZ2VMUFMtaGdLUHRJSGxrZzZfV2NveFRHb0tQNzlkX3dhSFl4R05sN1doU25laUJTeGJwYlFBS2syMWxnNzk4WGI3dlp5RUFURE1yUlI5TWVFNkFkajVISnBZM0NveVJBUENtYUtHUkNLNHVvWlNvSXUwaEZWbEtVUHliYncwMDBHTy13YTJLTjhVd2dJSW0waTVJMXVXOUdrcTR6akJ5NXpoZ3F1VVhiRzliV1BBT1lycTVRYTgxRHhHY0JsSnlIWUFwLUREUEU5VEdnNHpZbVhqSm54WnFIRWR1R3FkZXZaOFhNSTB1a2ZrR0lJMTR3VU9pTUlJSXJYbEVjQmZfNDZJOGdRV0R6eHljWmVfSkdYLUxBdWF5WHJ5clVGZWhWTlVkWlVsOXdYTmFKQi1rYUNxejVRd2FSOTNzR3ctUVNmdEQwTnZMZTdDeU9ILUU2dmc2U3RfTmVUdmd2OFluaENpWElsWjhIT2ZJd05lN3RFRl9VY3o1T2JQeWttM3R5bHJOVWp0MFZ5QW10dGFjVkkyaUdpaGNVUHJtazRsVklaN1ZEX0xTVy1pN3lvU3VydHBzUFhjZTJwS0RJbzMwbEpHaE9fM0tVbWwyU1VaQ3F6SjF5RW1LcHlzSDVIRFc5Y3NJRkNBM2RlQWpmWlV2TjdVIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiYWxnIjogIlJTMjU2IgogICAgICAgICB9LAogICAgICAgICAic2lnbmF0dXJlIjogIm5saTZIdzRrbW5KcTBSUmRXaGVfbkhZS2VJLVpKenM1U0d5SUpDakh1dWtnVzhBYklpVzFZYWJJR2NqWUt0QTY4dWN6T1hyUXZreGxWQXJLSlgzMDJzN0RpbzcxTlNPRzJVcnhsSjlibDFpd0F3a3ZyTEQ2T0p5MGxGLVg4WnRabXhPVmNQZmwzcmJwZFQ0dnlnWTdNcU1QRXdmb0IxTmlWZDYyZ1cxU2NSREZZcWw3R0FVaFVKNkp4QU15VzVaOXl5YVE0NV8wd0RMUk5mRjA5YWNXeVowTjRxVS1hZjhrUTZUUWZUX05ERzNCR3pRb2V3cHlEajRiMFBHb0diOFhLdDlwekpFdEdxM3lQM25VMFFBbk90a2gwTnZac1l1UFcyUnhDT3lRNEYzVlR3UkF2eF9HSTZrMVRpYmlKNnByUWluUy16Sjh6RE8zUjBuakE3OFBwNXcxcVpaUE9BdmtzZFNSYzJDcVMtcWhpTmF5YUhOVHpVNnpyOXlOZHR2S0o1QjNST0FmNUtjYXNiWURjTnVpeXBUNk90LUtqQ2I1dmYtWVpnc2FRNzJBdFBhSU4yeUpNREZHbmEwM0hpSjMxcTJRUlp5eTZrd3RYaGtwcDhTdEdIcHYxSWRaV09SVWttb0g5SFBzSGk4SExRLTZlM0tEY2x1RUQyMTNpZnljaVhtN0YzdHdaTTNHeDd1UXR1SldHaUlTZ2Z0QW9lVjZfUmI2VThkMmZxNzZuWHYxak5nckRRcE5waEZFd2tCdGRtZHZ2THByZVVYX3BWangza1AxN3pWbXFKNmNOOWkwWUc4WHg2VmRzcUxsRXUxQ2Rhd3Q0eko1M3VHMFlKTjRnUDZwc25yUS1uM0U1aFdlMDJ3d3dBZ3F3bGlPdmd4V1RTeXJyLXY2eDI0IiwKICAgICAgICAgInByb3RlY3RlZCI6ICJleUptYjNKdFlYUk1aVzVuZEdnaU9qRTNNeXdpWm05eWJXRjBWR0ZwYkNJNkltWlJJaXdpZEdsdFpTSTZJakl3TVRjdE1EVXRNRFZVTWpFNk5UYzZNek5hSW4wIgogICAgICB9CiAgIF0KfQ=="}`
) )
func TestActivateNoContainerd(t *testing.T) { func TestActivateNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient( testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) { func(string) (clitypes.ContainerizedClient, error) {
return nil, fmt.Errorf("some error") return nil, fmt.Errorf("some error")
}, },
) )
isRoot = func() bool { return true }
cmd := newActivateCommand(testCli) cmd := newActivateCommand(testCli)
cmd.Flags().Set("license", "invalidpath") cmd.Flags().Set("license", "invalidpath")
cmd.SilenceUsage = true cmd.SilenceUsage = true
@@ -24,10 +40,11 @@ func TestActivateNoContainerd(t *testing.T) {
func TestActivateBadLicense(t *testing.T) { func TestActivateBadLicense(t *testing.T) {
testCli.SetContainerizedEngineClient( testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) { func(string) (clitypes.ContainerizedClient, error) {
return &fakeContainerizedEngineClient{}, nil return &fakeContainerizedEngineClient{}, nil
}, },
) )
isRoot = func() bool { return true }
cmd := newActivateCommand(testCli) cmd := newActivateCommand(testCli)
cmd.SilenceUsage = true cmd.SilenceUsage = true
cmd.SilenceErrors = true cmd.SilenceErrors = true
@@ -35,3 +52,95 @@ func TestActivateBadLicense(t *testing.T) {
err := cmd.Execute() err := cmd.Execute()
assert.Error(t, err, "open invalidpath: no such file or directory") assert.Error(t, err, "open invalidpath: no such file or directory")
} }
func TestActivateExpiredLicenseDryRun(t *testing.T) {
dir := fs.NewDir(t, "license", fs.WithFile("docker.lic", expiredLicense, fs.WithMode(0644)))
defer dir.Remove()
filename := dir.Join("docker.lic")
isRoot = func() bool { return true }
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
c.SetContainerizedEngineClient(
func(string) (clitypes.ContainerizedClient, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
cmd := newActivateCommand(c)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
cmd.Flags().Set("license", filename)
cmd.Flags().Set("display-only", "true")
c.OutBuffer().Reset()
err := cmd.Execute()
assert.NilError(t, err)
golden.Assert(t, c.OutBuffer().String(), "expired-license-display-only.golden")
}
type mockLicenseClient struct{}
func (c mockLicenseClient) LoginViaAuth(ctx context.Context, username, password string) (authToken string, err error) {
return "", fmt.Errorf("not implemented")
}
func (c mockLicenseClient) GetHubUserOrgs(ctx context.Context, authToken string) (orgs []model.Org, err error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) GetHubUserByName(ctx context.Context, username string) (user *model.User, err error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) VerifyLicense(ctx context.Context, license model.IssuedLicense) (res *model.CheckResponse, err error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error) {
return "", fmt.Errorf("not implemented")
}
func (c mockLicenseClient) ListSubscriptions(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
expires := time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC)
return []*model.Subscription{
{
State: "active",
Expires: &expires,
},
}, nil
}
func (c mockLicenseClient) ListSubscriptionsDetails(ctx context.Context, authToken, dockerID string) (response []*model.SubscriptionDetail, err error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) DownloadLicenseFromHub(ctx context.Context, authToken, subscriptionID string) (license *model.IssuedLicense, err error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) ParseLicense(license []byte) (parsedLicense *model.IssuedLicense, err error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) StoreLicense(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error {
return fmt.Errorf("not implemented")
}
func (c mockLicenseClient) LoadLocalLicense(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error) {
return nil, fmt.Errorf("not implemented")
}
func (c mockLicenseClient) SummarizeLicense(res *model.CheckResponse, keyID string) *model.Subscription {
return nil
}
func TestActivateDisplayOnlyHub(t *testing.T) {
isRoot = func() bool { return true }
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
c.SetContainerizedEngineClient(
func(string) (clitypes.ContainerizedClient, error) {
return &fakeContainerizedEngineClient{}, nil
},
)
hubUser := licenseutils.HubUser{
Client: mockLicenseClient{},
}
options := activateOptions{
licenseLoginFunc: func(ctx context.Context, authConfig *types.AuthConfig) (licenseutils.HubUser, error) {
return hubUser, nil
},
displayOnly: true,
}
c.OutBuffer().Reset()
err := runActivate(c, options)
assert.NilError(t, err)
golden.Assert(t, c.OutBuffer().String(), "expired-hub-license-display-only.golden")
}
+13
View File
@@ -0,0 +1,13 @@
// +build !windows
package engine
import (
"golang.org/x/sys/unix"
)
var (
isRoot = func() bool {
return unix.Geteuid() == 0
}
)
+9
View File
@@ -0,0 +1,9 @@
// +build windows
package engine
var (
isRoot = func() bool {
return true
}
)
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
clitypes "github.com/docker/cli/types"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry" registrytypes "github.com/docker/docker/api/types/registry"
@@ -13,7 +14,7 @@ import (
func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) { func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) {
if registryPrefix == "" { if registryPrefix == "" {
registryPrefix = "docker.io/docker" registryPrefix = clitypes.RegistryPrefix
} }
distributionRef, err := reference.ParseNormalizedNamed(registryPrefix) distributionRef, err := reference.ParseNormalizedNamed(registryPrefix)
if err != nil { if err != nil {
+25 -33
View File
@@ -7,18 +7,16 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/containerizedengine" "github.com/docker/cli/internal/versions"
clitypes "github.com/docker/cli/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const (
releaseNotePrefix = "https://docs.docker.com/releasenotes"
)
type checkOptions struct { type checkOptions struct {
registryPrefix string registryPrefix string
preReleases bool preReleases bool
engineImage string
downgrades bool downgrades bool
upgrades bool upgrades bool
format string format string
@@ -38,9 +36,10 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
}, },
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&options.registryPrefix, "registry-prefix", "", "Override the existing location where engine images are pulled") flags.StringVar(&options.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the existing location where engine images are pulled")
flags.BoolVar(&options.downgrades, "downgrades", false, "Report downgrades (default omits older versions)") 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.preReleases, "pre-releases", false, "Include pre-release versions")
flags.StringVar(&options.engineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)")
flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades") flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades")
flags.StringVar(&options.format, "format", "", "Pretty-print updates using a Go template") flags.StringVar(&options.format, "format", "", "Pretty-print updates using a Go template")
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display available versions") flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display available versions")
@@ -50,54 +49,47 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
} }
func runCheck(dockerCli command.Cli, options checkOptions) error { func runCheck(dockerCli command.Cli, options checkOptions) error {
if !isRoot() {
return errors.New("this command must be run as a privileged user")
}
ctx := context.Background() ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath) client := dockerCli.Client()
if err != nil { serverVersion, err := client.ServerVersion(ctx)
return errors.Wrap(err, "unable to access local containerd")
}
defer client.Close()
currentOpts, err := client.GetCurrentEngineVersion(ctx)
if err != nil { if err != nil {
return err return err
} }
// override with user provided prefix if specified availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, options.engineImage, serverVersion.Version)
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 { if err != nil {
return err return err
} }
availUpdates := []containerizedengine.Update{ availUpdates := []clitypes.Update{
{Type: "current", Version: currentVersion}, {Type: "current", Version: serverVersion.Version},
} }
if len(versions.Patches) > 0 { if len(availVersions.Patches) > 0 {
availUpdates = append(availUpdates, availUpdates = append(availUpdates,
processVersions( processVersions(
currentVersion, serverVersion.Version,
"patch", "patch",
options.preReleases, options.preReleases,
versions.Patches)...) availVersions.Patches)...)
} }
if options.upgrades { if options.upgrades {
availUpdates = append(availUpdates, availUpdates = append(availUpdates,
processVersions( processVersions(
currentVersion, serverVersion.Version,
"upgrade", "upgrade",
options.preReleases, options.preReleases,
versions.Upgrades)...) availVersions.Upgrades)...)
} }
if options.downgrades { if options.downgrades {
availUpdates = append(availUpdates, availUpdates = append(availUpdates,
processVersions( processVersions(
currentVersion, serverVersion.Version,
"downgrade", "downgrade",
options.preReleases, options.preReleases,
versions.Downgrades)...) availVersions.Downgrades)...)
} }
format := options.format format := options.format
@@ -115,17 +107,17 @@ func runCheck(dockerCli command.Cli, options checkOptions) error {
func processVersions(currentVersion, verType string, func processVersions(currentVersion, verType string,
includePrerelease bool, includePrerelease bool,
versions []containerizedengine.DockerVersion) []containerizedengine.Update { availVersions []clitypes.DockerVersion) []clitypes.Update {
availUpdates := []containerizedengine.Update{} availUpdates := []clitypes.Update{}
for _, ver := range versions { for _, ver := range availVersions {
if !includePrerelease && ver.Prerelease() != "" { if !includePrerelease && ver.Prerelease() != "" {
continue continue
} }
if ver.Tag != currentVersion { if ver.Tag != currentVersion {
availUpdates = append(availUpdates, containerizedengine.Update{ availUpdates = append(availUpdates, clitypes.Update{
Type: verType, Type: verType,
Version: ver.Tag, Version: ver.Tag,
Notes: fmt.Sprintf("%s/%s", releaseNotePrefix, ver.Tag), Notes: fmt.Sprintf("%s?%s", clitypes.ReleaseNotePrefix, ver.Tag),
}) })
} }
} }
+67 -96
View File
@@ -5,11 +5,13 @@ import (
"fmt" "fmt"
"testing" "testing"
registryclient "github.com/docker/cli/cli/registry/client" manifesttypes "github.com/docker/cli/cli/manifest/types"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/distribution"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
ver "github.com/hashicorp/go-version" "github.com/opencontainers/go-digest"
"gotest.tools/assert" "gotest.tools/assert"
"gotest.tools/golden" "gotest.tools/golden"
) )
@@ -18,126 +20,95 @@ var (
testCli = test.NewFakeCli(&client.Client{}) testCli = test.NewFakeCli(&client.Client{})
) )
func TestCheckForUpdatesNoContainerd(t *testing.T) { type verClient struct {
testCli.SetContainerizedEngineClient( client.Client
func(string) (containerizedengine.Client, error) { ver types.Version
return nil, fmt.Errorf("some error") verErr error
}, info types.Info
) infoErr error
cmd := newCheckForUpdatesCommand(testCli) }
cmd.SilenceUsage = true
cmd.SilenceErrors = true func (c *verClient) ServerVersion(ctx context.Context) (types.Version, error) {
err := cmd.Execute() return c.ver, c.verErr
assert.ErrorContains(t, err, "unable to access local containerd") }
func (c *verClient) Info(ctx context.Context) (types.Info, error) {
return c.info, c.infoErr
}
type testRegistryClient struct {
tags []string
}
func (c testRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
return manifesttypes.ImageManifest{}, nil
}
func (c testRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
return nil, nil
}
func (c testRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
return nil
}
func (c testRegistryClient) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) {
return "", nil
}
func (c testRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([]string, error) {
return c.tags, nil
} }
func TestCheckForUpdatesNoCurrentVersion(t *testing.T) { func TestCheckForUpdatesNoCurrentVersion(t *testing.T) {
retErr := fmt.Errorf("some failure") isRoot = func() bool { return true }
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) { c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
return containerizedengine.EngineInitOptions{}, retErr c.SetRegistryClient(testRegistryClient{})
} cmd := newCheckForUpdatesCommand(c)
testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) {
return &fakeContainerizedEngineClient{
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
}, nil
},
)
cmd := newCheckForUpdatesCommand(testCli)
cmd.SilenceUsage = true cmd.SilenceUsage = true
cmd.SilenceErrors = true cmd.SilenceErrors = true
err := cmd.Execute() err := cmd.Execute()
assert.Assert(t, err == retErr) assert.ErrorContains(t, err, "no such file or directory")
}
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) { func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) { c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil})
return containerizedengine.EngineInitOptions{ c.SetRegistryClient(testRegistryClient{[]string{
EngineImage: "current engine", "1.0.1", "1.0.2", "1.0.3-beta1",
EngineVersion: "1.1.0", "1.1.1", "1.1.2", "1.1.3-beta1",
}, nil "1.2.0", "2.0.0", "2.1.0-beta1",
} }})
getEngineVersionsFunc := func(ctx context.Context,
registryClient registryclient.RegistryClient, isRoot = func() bool { return true }
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) { cmd := newCheckForUpdatesCommand(c)
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("pre-releases", "true")
cmd.Flags().Set("downgrades", "true") cmd.Flags().Set("downgrades", "true")
cmd.Flags().Set("engine-image", "engine-community")
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute() err := cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
golden.Assert(t, testCli.OutBuffer().String(), "check-all.golden") golden.Assert(t, c.OutBuffer().String(), "check-all.golden")
testCli.OutBuffer().Reset() c.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false") cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "true") cmd.Flags().Set("downgrades", "true")
err = cmd.Execute() err = cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String()) fmt.Println(c.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-no-prerelease.golden") golden.Assert(t, c.OutBuffer().String(), "check-no-prerelease.golden")
testCli.OutBuffer().Reset() c.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false") cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "false") cmd.Flags().Set("downgrades", "false")
err = cmd.Execute() err = cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String()) fmt.Println(c.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-no-downgrades.golden") golden.Assert(t, c.OutBuffer().String(), "check-no-downgrades.golden")
testCli.OutBuffer().Reset() c.OutBuffer().Reset()
cmd.Flags().Set("pre-releases", "false") cmd.Flags().Set("pre-releases", "false")
cmd.Flags().Set("downgrades", "false") cmd.Flags().Set("downgrades", "false")
cmd.Flags().Set("upgrades", "false") cmd.Flags().Set("upgrades", "false")
err = cmd.Execute() err = cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
fmt.Println(testCli.OutBuffer().String()) fmt.Println(c.OutBuffer().String())
golden.Assert(t, testCli.OutBuffer().String(), "check-patches-only.golden") golden.Assert(t, c.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
} }
+28 -32
View File
@@ -5,7 +5,7 @@ import (
"github.com/containerd/containerd" "github.com/containerd/containerd"
registryclient "github.com/docker/cli/cli/registry/client" registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/internal/containerizedengine" clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
) )
@@ -13,28 +13,26 @@ type (
fakeContainerizedEngineClient struct { fakeContainerizedEngineClient struct {
closeFunc func() error closeFunc func() error
activateEngineFunc func(ctx context.Context, activateEngineFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions, opts clitypes.EngineInitOptions,
out containerizedengine.OutStream, out clitypes.OutStream,
authConfig *types.AuthConfig, authConfig *types.AuthConfig) error
healthfn func(context.Context) error) error
initEngineFunc func(ctx context.Context, initEngineFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions, opts clitypes.EngineInitOptions,
out containerizedengine.OutStream, out clitypes.OutStream,
authConfig *types.AuthConfig, authConfig *types.AuthConfig,
healthfn func(context.Context) error) error healthfn func(context.Context) error) error
doUpdateFunc func(ctx context.Context, doUpdateFunc func(ctx context.Context,
opts containerizedengine.EngineInitOptions, opts clitypes.EngineInitOptions,
out containerizedengine.OutStream, out clitypes.OutStream,
authConfig *types.AuthConfig, authConfig *types.AuthConfig) error
healthfn func(context.Context) error) error
getEngineVersionsFunc func(ctx context.Context, getEngineVersionsFunc func(ctx context.Context,
registryClient registryclient.RegistryClient, registryClient registryclient.RegistryClient,
currentVersion, currentVersion,
imageName string) (containerizedengine.AvailableVersions, error) imageName string) (clitypes.AvailableVersions, error)
getEngineFunc func(ctx context.Context) (containerd.Container, error) getEngineFunc func(ctx context.Context) (containerd.Container, error)
removeEngineFunc func(ctx context.Context, engine containerd.Container) error removeEngineFunc func(ctx context.Context) error
getCurrentEngineVersionFunc func(ctx context.Context) (containerizedengine.EngineInitOptions, error) getCurrentEngineVersionFunc func(ctx context.Context) (clitypes.EngineInitOptions, error)
} }
) )
@@ -46,18 +44,17 @@ func (w *fakeContainerizedEngineClient) Close() error {
} }
func (w *fakeContainerizedEngineClient) ActivateEngine(ctx context.Context, func (w *fakeContainerizedEngineClient) ActivateEngine(ctx context.Context,
opts containerizedengine.EngineInitOptions, opts clitypes.EngineInitOptions,
out containerizedengine.OutStream, out clitypes.OutStream,
authConfig *types.AuthConfig, authConfig *types.AuthConfig) error {
healthfn func(context.Context) error) error {
if w.activateEngineFunc != nil { if w.activateEngineFunc != nil {
return w.activateEngineFunc(ctx, opts, out, authConfig, healthfn) return w.activateEngineFunc(ctx, opts, out, authConfig)
} }
return nil return nil
} }
func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context, func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
opts containerizedengine.EngineInitOptions, opts clitypes.EngineInitOptions,
out containerizedengine.OutStream, out clitypes.OutStream,
authConfig *types.AuthConfig, authConfig *types.AuthConfig,
healthfn func(context.Context) error) error { healthfn func(context.Context) error) error {
if w.initEngineFunc != nil { if w.initEngineFunc != nil {
@@ -66,23 +63,22 @@ func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
return nil return nil
} }
func (w *fakeContainerizedEngineClient) DoUpdate(ctx context.Context, func (w *fakeContainerizedEngineClient) DoUpdate(ctx context.Context,
opts containerizedengine.EngineInitOptions, opts clitypes.EngineInitOptions,
out containerizedengine.OutStream, out clitypes.OutStream,
authConfig *types.AuthConfig, authConfig *types.AuthConfig) error {
healthfn func(context.Context) error) error {
if w.doUpdateFunc != nil { if w.doUpdateFunc != nil {
return w.doUpdateFunc(ctx, opts, out, authConfig, healthfn) return w.doUpdateFunc(ctx, opts, out, authConfig)
} }
return nil return nil
} }
func (w *fakeContainerizedEngineClient) GetEngineVersions(ctx context.Context, func (w *fakeContainerizedEngineClient) GetEngineVersions(ctx context.Context,
registryClient registryclient.RegistryClient, registryClient registryclient.RegistryClient,
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) { currentVersion, imageName string) (clitypes.AvailableVersions, error) {
if w.getEngineVersionsFunc != nil { if w.getEngineVersionsFunc != nil {
return w.getEngineVersionsFunc(ctx, registryClient, currentVersion, imageName) return w.getEngineVersionsFunc(ctx, registryClient, currentVersion, imageName)
} }
return containerizedengine.AvailableVersions{}, nil return clitypes.AvailableVersions{}, nil
} }
func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containerd.Container, error) { func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containerd.Container, error) {
@@ -91,15 +87,15 @@ func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containe
} }
return nil, nil return nil, nil
} }
func (w *fakeContainerizedEngineClient) RemoveEngine(ctx context.Context, engine containerd.Container) error { func (w *fakeContainerizedEngineClient) RemoveEngine(ctx context.Context) error {
if w.removeEngineFunc != nil { if w.removeEngineFunc != nil {
return w.removeEngineFunc(ctx, engine) return w.removeEngineFunc(ctx)
} }
return nil return nil
} }
func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (containerizedengine.EngineInitOptions, error) { func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (clitypes.EngineInitOptions, error) {
if w.getCurrentEngineVersionFunc != nil { if w.getCurrentEngineVersionFunc != nil {
return w.getCurrentEngineVersionFunc(ctx) return w.getCurrentEngineVersionFunc(ctx)
} }
return containerizedengine.EngineInitOptions{}, nil return clitypes.EngineInitOptions{}, nil
} }
-2
View File
@@ -15,11 +15,9 @@ func NewEngineCommand(dockerCli command.Cli) *cobra.Command {
RunE: command.ShowHelp(dockerCli.Err()), RunE: command.ShowHelp(dockerCli.Err()),
} }
cmd.AddCommand( cmd.AddCommand(
newInitCommand(dockerCli),
newActivateCommand(dockerCli), newActivateCommand(dockerCli),
newCheckForUpdatesCommand(dockerCli), newCheckForUpdatesCommand(dockerCli),
newUpdateCommand(dockerCli), newUpdateCommand(dockerCli),
newRmCommand(dockerCli),
) )
return cmd return cmd
} }
+1 -1
View File
@@ -10,5 +10,5 @@ func TestNewEngineCommand(t *testing.T) {
cmd := NewEngineCommand(testCli) cmd := NewEngineCommand(testCli)
subcommands := cmd.Commands() subcommands := cmd.Commands()
assert.Assert(t, len(subcommands) == 5) assert.Assert(t, len(subcommands) == 3)
} }
+2 -54
View File
@@ -1,62 +1,10 @@
package engine package engine
import ( import (
"context" clitypes "github.com/docker/cli/types"
"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 { type extendedEngineInitOptions struct {
containerizedengine.EngineInitOptions clitypes.EngineInitOptions
sockPath string 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
})
}
-33
View File
@@ -1,33 +0,0 @@
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
View File
@@ -1,54 +0,0 @@
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)
}
-33
View File
@@ -1,33 +0,0 @@
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)
}
+9 -9
View File
@@ -1,11 +1,11 @@
TYPE VERSION NOTES TYPE VERSION NOTES
current 1.1.0 current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1 patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2 patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
patch 1.1.3-beta1 https://docs.docker.com/releasenotes/1.1.3-beta1 patch 1.1.3-beta1 https://docker.com/engine/releasenotes?1.1.3-beta1
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0 upgrade 1.2.0 https://docker.com/engine/releasenotes?1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0 upgrade 2.0.0 https://docker.com/engine/releasenotes?2.0.0
upgrade 2.1.0-beta1 https://docs.docker.com/releasenotes/2.1.0-beta1 upgrade 2.1.0-beta1 https://docker.com/engine/releasenotes?2.1.0-beta1
downgrade 1.0.1 https://docs.docker.com/releasenotes/1.0.1 downgrade 1.0.1 https://docker.com/engine/releasenotes?1.0.1
downgrade 1.0.2 https://docs.docker.com/releasenotes/1.0.2 downgrade 1.0.2 https://docker.com/engine/releasenotes?1.0.2
downgrade 1.0.3-beta1 https://docs.docker.com/releasenotes/1.0.3-beta1 downgrade 1.0.3-beta1 https://docker.com/engine/releasenotes?1.0.3-beta1
+4 -4
View File
@@ -1,6 +1,6 @@
TYPE VERSION NOTES TYPE VERSION NOTES
current 1.1.0 current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1 patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2 patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0 upgrade 1.2.0 https://docker.com/engine/releasenotes?1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0 upgrade 2.0.0 https://docker.com/engine/releasenotes?2.0.0
+6 -6
View File
@@ -1,8 +1,8 @@
TYPE VERSION NOTES TYPE VERSION NOTES
current 1.1.0 current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1 patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2 patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
upgrade 1.2.0 https://docs.docker.com/releasenotes/1.2.0 upgrade 1.2.0 https://docker.com/engine/releasenotes?1.2.0
upgrade 2.0.0 https://docs.docker.com/releasenotes/2.0.0 upgrade 2.0.0 https://docker.com/engine/releasenotes?2.0.0
downgrade 1.0.1 https://docs.docker.com/releasenotes/1.0.1 downgrade 1.0.1 https://docker.com/engine/releasenotes?1.0.1
downgrade 1.0.2 https://docs.docker.com/releasenotes/1.0.2 downgrade 1.0.2 https://docker.com/engine/releasenotes?1.0.2
+2 -2
View File
@@ -1,4 +1,4 @@
TYPE VERSION NOTES TYPE VERSION NOTES
current 1.1.0 current 1.1.0
patch 1.1.1 https://docs.docker.com/releasenotes/1.1.1 patch 1.1.1 https://docker.com/engine/releasenotes?1.1.1
patch 1.1.2 https://docs.docker.com/releasenotes/1.1.2 patch 1.1.2 https://docker.com/engine/releasenotes?1.1.2
@@ -0,0 +1,3 @@
Looking for existing licenses for ...
NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS
0 2010-01-01 00:00:00 +0000 UTC
@@ -0,0 +1 @@
License: Quantity: 1 Nodes Expiration date: 2018-03-18 Expired! You will no longer receive updates. Please renew at https://docker.com/licensing
+9 -22
View File
@@ -6,6 +6,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
clitypes "github.com/docker/cli/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -24,45 +25,31 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version") flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version")
flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image") flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)")
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "", "Override the current location where engine images are pulled") flags.StringVar(&options.RegistryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the current location where engine images are pulled")
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint") flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
return cmd return cmd
} }
func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error { func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error {
if !isRoot() {
return errors.New("this command must be run as a privileged user")
}
ctx := context.Background() ctx := context.Background()
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath) client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
if err != nil { if err != nil {
return errors.Wrap(err, "unable to access local containerd") return errors.Wrap(err, "unable to access local containerd")
} }
defer client.Close() 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) authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
if err != nil { if err != nil {
return err return err
} }
if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig); err != nil {
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 return err
} }
fmt.Fprintln(dockerCli.Out(), "Success! The docker engine is now running.") fmt.Fprintln(dockerCli.Out(), `Successfully updated engine.
Restart docker with 'systemctl restart docker' to complete the update.`)
return nil return nil
} }
+11 -6
View File
@@ -4,13 +4,16 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/docker/cli/internal/containerizedengine" "github.com/docker/cli/internal/test"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"gotest.tools/assert" "gotest.tools/assert"
) )
func TestUpdateNoContainerd(t *testing.T) { func TestUpdateNoContainerd(t *testing.T) {
testCli.SetContainerizedEngineClient( testCli.SetContainerizedEngineClient(
func(string) (containerizedengine.Client, error) { func(string) (clitypes.ContainerizedClient, error) {
return nil, fmt.Errorf("some error") return nil, fmt.Errorf("some error")
}, },
) )
@@ -22,14 +25,16 @@ func TestUpdateNoContainerd(t *testing.T) {
} }
func TestUpdateHappy(t *testing.T) { func TestUpdateHappy(t *testing.T) {
testCli.SetContainerizedEngineClient( c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil})
func(string) (containerizedengine.Client, error) { c.SetContainerizedEngineClient(
func(string) (clitypes.ContainerizedClient, error) {
return &fakeContainerizedEngineClient{}, nil return &fakeContainerizedEngineClient{}, nil
}, },
) )
cmd := newUpdateCommand(testCli) cmd := newUpdateCommand(c)
cmd.Flags().Set("registry-prefix", "docker.io/docker") cmd.Flags().Set("registry-prefix", clitypes.RegistryPrefix)
cmd.Flags().Set("version", "someversion") cmd.Flags().Set("version", "someversion")
cmd.Flags().Set("engine-image", "someimage")
err := cmd.Execute() err := cmd.Execute()
assert.NilError(t, err) assert.NilError(t, err)
} }
+179
View File
@@ -0,0 +1,179 @@
package formatter
import (
"fmt"
"sort"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
const (
defaultBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}\t{{.Description}}"
cacheIDHeader = "CACHE ID"
cacheTypeHeader = "CACHE TYPE"
parentHeader = "PARENT"
lastUsedSinceHeader = "LAST USED"
usageCountHeader = "USAGE"
inUseHeader = "IN USE"
sharedHeader = "SHARED"
)
// NewBuildCacheFormat returns a Format for rendering using a Context
func NewBuildCacheFormat(source string, quiet bool) Format {
switch source {
case TableFormatKey:
if quiet {
return defaultQuietFormat
}
return Format(defaultBuildCacheTableFormat)
case RawFormatKey:
if quiet {
return `build_cache_id: {{.ID}}`
}
format := `build_cache_id: {{.ID}}
parent_id: {{.Parent}}
build_cache_type: {{.CacheType}}
description: {{.Description}}
created_at: {{.CreatedAt}}
created_since: {{.CreatedSince}}
last_used_at: {{.LastUsedAt}}
last_used_since: {{.LastUsedSince}}
usage_count: {{.UsageCount}}
in_use: {{.InUse}}
shared: {{.Shared}}
`
return Format(format)
}
return Format(source)
}
func buildCacheSort(buildCache []*types.BuildCache) {
sort.Slice(buildCache, func(i, j int) bool {
lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt
switch {
case lui == nil && luj == nil:
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
case lui == nil:
return true
case luj == nil:
return false
case lui.Equal(*luj):
return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0
default:
return lui.Before(*luj)
}
})
}
// BuildCacheWrite renders the context for a list of containers
func BuildCacheWrite(ctx Context, buildCaches []*types.BuildCache) error {
render := func(format func(subContext subContext) error) error {
buildCacheSort(buildCaches)
for _, bc := range buildCaches {
err := format(&buildCacheContext{trunc: ctx.Trunc, v: bc})
if err != nil {
return err
}
}
return nil
}
return ctx.Write(newBuildCacheContext(), render)
}
type buildCacheHeaderContext map[string]string
type buildCacheContext struct {
HeaderContext
trunc bool
v *types.BuildCache
}
func newBuildCacheContext() *buildCacheContext {
buildCacheCtx := buildCacheContext{}
buildCacheCtx.header = buildCacheHeaderContext{
"ID": cacheIDHeader,
"Parent": parentHeader,
"CacheType": cacheTypeHeader,
"Size": sizeHeader,
"CreatedSince": createdSinceHeader,
"LastUsedSince": lastUsedSinceHeader,
"UsageCount": usageCountHeader,
"InUse": inUseHeader,
"Shared": sharedHeader,
"Description": descriptionHeader,
}
return &buildCacheCtx
}
func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
return marshalJSON(c)
}
func (c *buildCacheContext) ID() string {
id := c.v.ID
if c.trunc {
id = stringid.TruncateID(c.v.ID)
}
if c.v.InUse {
return id + "*"
}
return id
}
func (c *buildCacheContext) Parent() string {
if c.trunc {
return stringid.TruncateID(c.v.Parent)
}
return c.v.Parent
}
func (c *buildCacheContext) CacheType() string {
return c.v.Type
}
func (c *buildCacheContext) Description() string {
return c.v.Description
}
func (c *buildCacheContext) Size() string {
return units.HumanSizeWithPrecision(float64(c.v.Size), 3)
}
func (c *buildCacheContext) CreatedAt() string {
return c.v.CreatedAt.String()
}
func (c *buildCacheContext) CreatedSince() string {
return units.HumanDuration(time.Now().UTC().Sub(c.v.CreatedAt)) + " ago"
}
func (c *buildCacheContext) LastUsedAt() string {
if c.v.LastUsedAt == nil {
return ""
}
return c.v.LastUsedAt.String()
}
func (c *buildCacheContext) LastUsedSince() string {
if c.v.LastUsedAt == nil {
return ""
}
return units.HumanDuration(time.Now().UTC().Sub(*c.v.LastUsedAt)) + " ago"
}
func (c *buildCacheContext) UsageCount() string {
return fmt.Sprintf("%d", c.v.UsageCount)
}
func (c *buildCacheContext) InUse() string {
return fmt.Sprintf("%t", c.v.InUse)
}
func (c *buildCacheContext) Shared() string {
return fmt.Sprintf("%t", c.v.Shared)
}
+102 -51
View File
@@ -12,19 +12,11 @@ import (
) )
const ( const (
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}" defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}" defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}"
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}" defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}" defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"
defaultBuildCacheVerboseFormat = ` defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
ID: {{.ID}}
Description: {{.Description}}
Mutable: {{.Mutable}}
Size: {{.Size}}
CreatedAt: {{.CreatedAt}}
LastUsedAt: {{.LastUsedAt}}
UsageCount: {{.UsageCount}}
`
typeHeader = "TYPE" typeHeader = "TYPE"
totalHeader = "TOTAL" totalHeader = "TOTAL"
@@ -32,7 +24,7 @@ UsageCount: {{.UsageCount}}
reclaimableHeader = "RECLAIMABLE" reclaimableHeader = "RECLAIMABLE"
containersHeader = "CONTAINERS" containersHeader = "CONTAINERS"
sharedSizeHeader = "SHARED SIZE" sharedSizeHeader = "SHARED SIZE"
uniqueSizeHeader = "UNIQUE SiZE" uniqueSizeHeader = "UNIQUE SIZE"
) )
// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct. // DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct.
@@ -56,14 +48,26 @@ func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template,
return ctx.parseFormat() return ctx.parseFormat()
} }
//
// NewDiskUsageFormat returns a format for rendering an DiskUsageContext // NewDiskUsageFormat returns a format for rendering an DiskUsageContext
func NewDiskUsageFormat(source string) Format { func NewDiskUsageFormat(source string, verbose bool) Format {
switch source { switch {
case TableFormatKey: case verbose && source == RawFormatKey:
format := defaultDiskUsageTableFormat format := `{{range .Images}}type: Image
return Format(format) ` + NewImageFormat(source, false, true) + `
case RawFormatKey: {{end -}}
{{range .Containers}}type: Container
` + NewContainerFormat(source, false, true) + `
{{end -}}
{{range .Volumes}}type: Volume
` + NewVolumeFormat(source, false) + `
{{end -}}
{{range .BuildCache}}type: Build Cache
` + NewBuildCacheFormat(source, false) + `
{{end -}}`
return format
case !verbose && source == TableFormatKey:
return Format(defaultDiskUsageTableFormat)
case !verbose && source == RawFormatKey:
format := `type: {{.Type}} format := `type: {{.Type}}
total: {{.TotalCount}} total: {{.TotalCount}}
active: {{.Active}} active: {{.Active}}
@@ -71,8 +75,9 @@ size: {{.Size}}
reclaimable: {{.Reclaimable}} reclaimable: {{.Reclaimable}}
` `
return Format(format) return Format(format)
default:
return Format(source)
} }
return Format(source)
} }
func (ctx *DiskUsageContext) Write() (err error) { func (ctx *DiskUsageContext) Write() (err error) {
@@ -129,14 +134,23 @@ func (ctx *DiskUsageContext) Write() (err error) {
return err return err
} }
func (ctx *DiskUsageContext) verboseWrite() error { type diskUsageContext struct {
// First images Images []*imageContext
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat) Containers []*containerContext
if err != nil { Volumes []*volumeContext
return err BuildCache []*buildCacheContext
} }
ctx.Output.Write([]byte("Images space usage:\n\n")) func (ctx *DiskUsageContext) verboseWrite() error {
duc := &diskUsageContext{
Images: make([]*imageContext, 0, len(ctx.Images)),
Containers: make([]*containerContext, 0, len(ctx.Containers)),
Volumes: make([]*volumeContext, 0, len(ctx.Volumes)),
BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
}
trunc := ctx.Format.IsTable()
// First images
for _, i := range ctx.Images { for _, i := range ctx.Images {
repo := "<none>" repo := "<none>"
tag := "<none>" tag := "<none>"
@@ -152,55 +166,92 @@ func (ctx *DiskUsageContext) verboseWrite() error {
} }
} }
err := ctx.contextFormat(tmpl, &imageContext{ duc.Images = append(duc.Images, &imageContext{
repo: repo, repo: repo,
tag: tag, tag: tag,
trunc: true, trunc: trunc,
i: *i, i: *i,
}) })
if err != nil { }
// Now containers
for _, c := range ctx.Containers {
// Don't display the virtual size
c.SizeRootFs = 0
duc.Containers = append(duc.Containers, &containerContext{trunc: trunc, c: *c})
}
// And volumes
for _, v := range ctx.Volumes {
duc.Volumes = append(duc.Volumes, &volumeContext{v: *v})
}
// And build cache
buildCacheSort(ctx.BuildCache)
for _, v := range ctx.BuildCache {
duc.BuildCache = append(duc.BuildCache, &buildCacheContext{v: v, trunc: trunc})
}
if ctx.Format == TableFormatKey {
return ctx.verboseWriteTable(duc)
}
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
return err
}
return tmpl.Execute(ctx.Output, duc)
}
func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error {
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
if err != nil {
return err
}
ctx.Output.Write([]byte("Images space usage:\n\n"))
for _, img := range duc.Images {
if err := ctx.contextFormat(tmpl, img); err != nil {
return err return err
} }
} }
ctx.postFormat(tmpl, newImageContext()) ctx.postFormat(tmpl, newImageContext())
// Now containers
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat) tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
if err != nil { if err != nil {
return err return err
} }
for _, c := range ctx.Containers { ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
// Don't display the virtual size for _, c := range duc.Containers {
c.SizeRootFs = 0 if err := ctx.contextFormat(tmpl, c); err != nil {
err := ctx.contextFormat(tmpl, &containerContext{trunc: true, c: *c})
if err != nil {
return err return err
} }
} }
ctx.postFormat(tmpl, newContainerContext()) ctx.postFormat(tmpl, newContainerContext())
// And volumes
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat) tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
if err != nil { if err != nil {
return err return err
} }
for _, v := range ctx.Volumes { ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
if err := ctx.contextFormat(tmpl, &volumeContext{v: *v}); err != nil { for _, v := range duc.Volumes {
if err := ctx.contextFormat(tmpl, v); err != nil {
return err return err
} }
} }
ctx.postFormat(tmpl, newVolumeContext()) ctx.postFormat(tmpl, newVolumeContext())
// And build cache tmpl, err = ctx.startSubsection(defaultDiskUsageBuildCacheTableFormat)
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize))) if err != nil {
return err
t := template.Must(template.New("buildcache").Parse(defaultBuildCacheVerboseFormat))
for _, v := range ctx.BuildCache {
t.Execute(ctx.Output, *v)
} }
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
for _, v := range duc.BuildCache {
if err := ctx.contextFormat(tmpl, v); err != nil {
return err
}
}
ctx.postFormat(tmpl, newBuildCacheContext())
return nil return nil
} }
@@ -416,7 +467,7 @@ func (c *diskUsageBuilderContext) Size() string {
func (c *diskUsageBuilderContext) Reclaimable() string { func (c *diskUsageBuilderContext) Reclaimable() string {
var inUseBytes int64 var inUseBytes int64
for _, bc := range c.buildCache { for _, bc := range c.buildCache {
if bc.InUse { if bc.InUse && !bc.Shared {
inUseBytes += bc.Size inUseBytes += bc.Size
} }
} }
+16 -7
View File
@@ -18,7 +18,7 @@ func TestDiskUsageContextFormatWrite(t *testing.T) {
{ {
DiskUsageContext{ DiskUsageContext{
Context: Context{ Context: Context{
Format: NewDiskUsageFormat("table"), Format: NewDiskUsageFormat("table", false),
}, },
Verbose: false}, Verbose: false},
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE `TYPE TOTAL ACTIVE SIZE RECLAIMABLE
@@ -29,14 +29,14 @@ Build Cache 0 0 0B
`, `,
}, },
{ {
DiskUsageContext{Verbose: true}, DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("table", true)}},
`Images space usage: `Images space usage:
REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS
Containers space usage: Containers space usage:
CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED STATUS NAMES
Local Volumes space usage: Local Volumes space usage:
@@ -44,8 +44,17 @@ VOLUME NAME LINKS SIZE
Build cache usage: 0B Build cache usage: 0B
CACHE ID CACHE TYPE SIZE CREATED LAST USED USAGE SHARED
`, `,
}, },
{
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("raw", true)}},
``,
},
{
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("{{json .}}", true)}},
`{"Images":[],"Containers":[],"Volumes":[],"BuildCache":[]}`,
},
// Errors // Errors
{ {
DiskUsageContext{ DiskUsageContext{
@@ -69,7 +78,7 @@ Build cache usage: 0B
{ {
DiskUsageContext{ DiskUsageContext{
Context: Context{ Context: Context{
Format: NewDiskUsageFormat("table"), Format: NewDiskUsageFormat("table", false),
}, },
}, },
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE `TYPE TOTAL ACTIVE SIZE RECLAIMABLE
@@ -82,7 +91,7 @@ Build Cache 0 0 0B
{ {
DiskUsageContext{ DiskUsageContext{
Context: Context{ Context: Context{
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"), Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}", false),
}, },
}, },
string(golden.Get(t, "disk-usage-context-write-custom.golden")), string(golden.Get(t, "disk-usage-context-write-custom.golden")),
@@ -91,7 +100,7 @@ Build Cache 0 0 0B
{ {
DiskUsageContext{ DiskUsageContext{
Context: Context{ Context: Context{
Format: NewDiskUsageFormat("raw"), Format: NewDiskUsageFormat("raw", false),
}, },
}, },
string(golden.Get(t, "disk-usage-raw-format.golden")), string(golden.Get(t, "disk-usage-raw-format.golden")),
+3 -3
View File
@@ -1,7 +1,7 @@
package formatter package formatter
import ( import (
"github.com/docker/cli/internal/containerizedengine" clitypes "github.com/docker/cli/types"
) )
const ( const (
@@ -31,7 +31,7 @@ func NewUpdatesFormat(source string, quiet bool) Format {
} }
// UpdatesWrite writes the context // UpdatesWrite writes the context
func UpdatesWrite(ctx Context, availableUpdates []containerizedengine.Update) error { func UpdatesWrite(ctx Context, availableUpdates []clitypes.Update) error {
render := func(format func(subContext subContext) error) error { render := func(format func(subContext subContext) error) error {
for _, update := range availableUpdates { for _, update := range availableUpdates {
updatesCtx := &updateContext{trunc: ctx.Trunc, u: update} updatesCtx := &updateContext{trunc: ctx.Trunc, u: update}
@@ -53,7 +53,7 @@ func UpdatesWrite(ctx Context, availableUpdates []containerizedengine.Update) er
type updateContext struct { type updateContext struct {
HeaderContext HeaderContext
trunc bool trunc bool
u containerizedengine.Update u clitypes.Update
} }
func (c *updateContext) MarshalJSON() ([]byte, error) { func (c *updateContext) MarshalJSON() ([]byte, error) {
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/internal/containerizedengine" clitypes "github.com/docker/cli/types"
"gotest.tools/assert" "gotest.tools/assert"
is "gotest.tools/assert/cmp" is "gotest.tools/assert/cmp"
) )
@@ -84,7 +84,7 @@ version2
} }
for _, testcase := range cases { for _, testcase := range cases {
updates := []containerizedengine.Update{ updates := []clitypes.Update{
{Type: "updateType1", Version: "version1", Notes: "description 1"}, {Type: "updateType1", Version: "version1", Notes: "description 1"},
{Type: "updateType2", Version: "version2", Notes: "description 2"}, {Type: "updateType2", Version: "version2", Notes: "description 2"},
} }
@@ -100,7 +100,7 @@ version2
} }
func TestUpdateContextWriteJSON(t *testing.T) { func TestUpdateContextWriteJSON(t *testing.T) {
updates := []containerizedengine.Update{ updates := []clitypes.Update{
{Type: "updateType1", Version: "version1", Notes: "note1"}, {Type: "updateType1", Version: "version1", Notes: "note1"},
{Type: "updateType2", Version: "version2", Notes: "note2"}, {Type: "updateType2", Version: "version2", Notes: "note2"},
} }
@@ -124,7 +124,7 @@ func TestUpdateContextWriteJSON(t *testing.T) {
} }
func TestUpdateContextWriteJSONField(t *testing.T) { func TestUpdateContextWriteJSONField(t *testing.T) {
updates := []containerizedengine.Update{ updates := []clitypes.Update{
{Type: "updateType1", Version: "version1"}, {Type: "updateType1", Version: "version1"},
{Type: "updateType2", Version: "version2"}, {Type: "updateType2", Version: "version2"},
} }
+17 -13
View File
@@ -13,7 +13,6 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"strconv"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
@@ -73,6 +72,7 @@ type buildOptions struct {
platform string platform string
untrusted bool untrusted bool
secrets []string secrets []string
ssh []string
} }
// dockerfileFromStdin returns true when the user specified that the Dockerfile // dockerfileFromStdin returns true when the user specified that the Dockerfile
@@ -136,6 +136,8 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image")
flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources")
flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip")
flags.SetAnnotation("compress", "no-buildkit", nil)
flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options")
flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build")
flags.SetAnnotation("network", "version", []string{"1.25"}) flags.SetAnnotation("network", "version", []string{"1.25"})
@@ -153,11 +155,18 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
flags.BoolVar(&options.stream, "stream", false, "Stream attaches to server to negotiate build context") flags.BoolVar(&options.stream, "stream", false, "Stream attaches to server to negotiate build context")
flags.SetAnnotation("stream", "experimental", nil) flags.SetAnnotation("stream", "experimental", nil)
flags.SetAnnotation("stream", "version", []string{"1.31"}) flags.SetAnnotation("stream", "version", []string{"1.31"})
flags.SetAnnotation("stream", "no-buildkit", nil)
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.StringVar(&options.progress, "progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output")
flags.SetAnnotation("progress", "buildkit", nil)
flags.StringArrayVar(&options.secrets, "secret", []string{}, "Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret") 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"}) flags.SetAnnotation("secret", "version", []string{"1.39"})
flags.SetAnnotation("secret", "buildkit", nil)
flags.StringArrayVar(&options.ssh, "ssh", []string{}, "SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|<id>[=<socket>|<key>[,<key>]])")
flags.SetAnnotation("ssh", "version", []string{"1.39"})
flags.SetAnnotation("ssh", "buildkit", nil)
return cmd return cmd
} }
@@ -179,22 +188,17 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error {
// nolint: gocyclo // nolint: gocyclo
func runBuild(dockerCli command.Cli, options buildOptions) error { func runBuild(dockerCli command.Cli, options buildOptions) error {
if buildkitEnv := os.Getenv("DOCKER_BUILDKIT"); buildkitEnv != "" { buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
enableBuildkit, err := strconv.ParseBool(buildkitEnv) if err != nil {
if err != nil { return err
return errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value") }
} if buildkitEnabled {
if enableBuildkit {
return runBuildBuildKit(dockerCli, options)
}
} else if dockerCli.ServerInfo().BuildkitVersion == types.BuilderBuildKit {
return runBuildBuildKit(dockerCli, options) return runBuildBuildKit(dockerCli, options)
} }
var ( var (
buildCtx io.ReadCloser buildCtx io.ReadCloser
dockerfileCtx io.ReadCloser dockerfileCtx io.ReadCloser
err error
contextDir string contextDir string
tempDir string tempDir string
relDockerfile string relDockerfile string
@@ -346,7 +350,7 @@ func runBuild(dockerCli command.Cli, options buildOptions) error {
buildCtx = dockerfileCtx buildCtx = dockerfileCtx
} }
s, err := trySession(dockerCli, contextDir) s, err := trySession(dockerCli, contextDir, true)
if err != nil { if err != nil {
return err return err
} }
+34 -3
View File
@@ -27,10 +27,11 @@ import (
"github.com/moby/buildkit/session/auth/authprovider" "github.com/moby/buildkit/session/auth/authprovider"
"github.com/moby/buildkit/session/filesync" "github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/session/secrets/secretsprovider" "github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/session/sshforward/sshprovider"
"github.com/moby/buildkit/util/appcontext" "github.com/moby/buildkit/util/appcontext"
"github.com/moby/buildkit/util/progress/progressui" "github.com/moby/buildkit/util/progress/progressui"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/tonistiigi/fsutil" fsutiltypes "github.com/tonistiigi/fsutil/types"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@@ -42,7 +43,7 @@ var errDockerfileConflict = errors.New("ambiguous Dockerfile source: both stdin
func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error { func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
ctx := appcontext.Context() ctx := appcontext.Context()
s, err := trySession(dockerCli, options.context) s, err := trySession(dockerCli, options.context, false)
if err != nil { if err != nil {
return err return err
} }
@@ -138,6 +139,13 @@ func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
} }
s.Allow(sp) s.Allow(sp)
} }
if len(options.ssh) > 0 {
sshp, err := parseSSHSpecs(options.ssh)
if err != nil {
return errors.Wrapf(err, "could not parse ssh: %v", options.ssh)
}
s.Allow(sshp)
}
eg, ctx := errgroup.WithContext(ctx) eg, ctx := errgroup.WithContext(ctx)
@@ -291,7 +299,7 @@ func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, opt
return err return err
} }
func resetUIDAndGID(s *fsutil.Stat) bool { func resetUIDAndGID(s *fsutiltypes.Stat) bool {
s.Uid = 0 s.Uid = 0
s.Gid = 0 s.Gid = 0
return true return true
@@ -408,3 +416,26 @@ func parseSecret(value string) (*secretsprovider.FileSource, error) {
} }
return &fs, nil return &fs, nil
} }
func parseSSHSpecs(sl []string) (session.Attachable, error) {
configs := make([]sshprovider.AgentConfig, 0, len(sl))
for _, v := range sl {
c, err := parseSSH(v)
if err != nil {
return nil, err
}
configs = append(configs, *c)
}
return sshprovider.NewSSHAgentProvider(configs)
}
func parseSSH(value string) (*sshprovider.AgentConfig, error) {
parts := strings.SplitN(value, "=", 2)
cfg := sshprovider.AgentConfig{
ID: parts[0],
}
if len(parts) > 1 {
cfg.Paths = strings.Split(parts[1], ",")
}
return &cfg, nil
}
+4 -4
View File
@@ -27,16 +27,16 @@ import (
const clientSessionRemote = "client-session" const clientSessionRemote = "client-session"
func isSessionSupported(dockerCli command.Cli) bool { func isSessionSupported(dockerCli command.Cli, forStream bool) bool {
if versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") { if !forStream && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.39") {
return true return true
} }
return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31") return dockerCli.ServerInfo().HasExperimental && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
} }
func trySession(dockerCli command.Cli, contextDir string) (*session.Session, error) { func trySession(dockerCli command.Cli, contextDir string, forStream bool) (*session.Session, error) {
var s *session.Session var s *session.Session
if isSessionSupported(dockerCli) { if isSessionSupported(dockerCli, forStream) {
sharedKey, err := getBuildSharedKey(contextDir) sharedKey, err := getBuildSharedKey(contextDir)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to get build shared key") return nil, errors.Wrap(err, "failed to get build shared key")
+5 -2
View File
@@ -19,6 +19,7 @@ type importOptions struct {
reference string reference string
changes dockeropts.ListOpts changes dockeropts.ListOpts
message string message string
platform string
} }
// NewImportCommand creates a new `docker import` command // NewImportCommand creates a new `docker import` command
@@ -43,6 +44,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command {
options.changes = dockeropts.NewListOpts(nil) options.changes = dockeropts.NewListOpts(nil)
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image") flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image") flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image")
command.AddPlatformFlag(flags, &options.platform)
return cmd return cmd
} }
@@ -71,8 +73,9 @@ func runImport(dockerCli command.Cli, options importOptions) error {
} }
importOptions := types.ImageImportOptions{ importOptions := types.ImageImportOptions{
Message: options.message, Message: options.message,
Changes: options.changes.GetAll(), Changes: options.changes.GetAll(),
Platform: options.platform,
} }
clnt := dockerCli.Client() clnt := dockerCli.Client()
+10 -3
View File
@@ -3,6 +3,7 @@ package image
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@@ -73,14 +74,20 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
} }
if len(report.ImagesDeleted) > 0 { if len(report.ImagesDeleted) > 0 {
output = "Deleted Images:\n" var sb strings.Builder
sb.WriteString("Deleted Images:\n")
for _, st := range report.ImagesDeleted { for _, st := range report.ImagesDeleted {
if st.Untagged != "" { if st.Untagged != "" {
output += fmt.Sprintln("untagged:", st.Untagged) sb.WriteString("untagged: ")
sb.WriteString(st.Untagged)
sb.WriteByte('\n')
} else { } else {
output += fmt.Sprintln("deleted:", st.Deleted) sb.WriteString("deleted: ")
sb.WriteString(st.Deleted)
sb.WriteByte('\n')
} }
} }
output = sb.String()
spaceReclaimed = report.SpaceReclaimed spaceReclaimed = report.SpaceReclaimed
} }
+1 -1
View File
@@ -70,7 +70,7 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (output string, err e
// RunPrune calls the Network Prune API // RunPrune calls the Network Prune API
// This returns the amount of space reclaimed and a detailed output string // This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) { func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter}) output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter})
return 0, output, err return 0, output, err
} }
+9 -7
View File
@@ -2,7 +2,6 @@ package system
import ( import (
"context" "context"
"errors"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
@@ -38,10 +37,6 @@ func newDiskUsageCommand(dockerCli command.Cli) *cobra.Command {
} }
func runDiskUsage(dockerCli command.Cli, opts diskUsageOptions) error { func runDiskUsage(dockerCli command.Cli, opts diskUsageOptions) error {
if opts.verbose && len(opts.format) != 0 {
return errors.New("the verbose and the format options conflict")
}
du, err := dockerCli.Client().DiskUsage(context.Background()) du, err := dockerCli.Client().DiskUsage(context.Background())
if err != nil { if err != nil {
return err return err
@@ -52,13 +47,20 @@ func runDiskUsage(dockerCli command.Cli, opts diskUsageOptions) error {
format = formatter.TableFormatKey format = formatter.TableFormatKey
} }
var bsz int64
for _, bc := range du.BuildCache {
if !bc.Shared {
bsz += bc.Size
}
}
duCtx := formatter.DiskUsageContext{ duCtx := formatter.DiskUsageContext{
Context: formatter.Context{ Context: formatter.Context{
Output: dockerCli.Out(), Output: dockerCli.Out(),
Format: formatter.NewDiskUsageFormat(format), Format: formatter.NewDiskUsageFormat(format, opts.verbose),
}, },
LayersSize: du.LayersSize, LayersSize: du.LayersSize,
BuilderSize: du.BuilderSize, BuilderSize: bsz,
BuildCache: du.BuildCache, BuildCache: du.BuildCache,
Images: du.Images, Images: du.Images,
Containers: du.Containers, Containers: du.Containers,
+3
View File
@@ -204,6 +204,9 @@ func prettyPrintInfo(dockerCli command.Cli, info types.Info) error {
} }
fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled) fmt.Fprintln(dockerCli.Out(), "Live Restore Enabled:", info.LiveRestoreEnabled)
if info.ProductLicense != "" {
fmt.Fprintln(dockerCli.Out(), "Product License:", info.ProductLicense)
}
fmt.Fprint(dockerCli.Out(), "\n") fmt.Fprint(dockerCli.Out(), "\n")
printWarnings(dockerCli, info) printWarnings(dockerCli, info)
+14 -23
View File
@@ -2,12 +2,12 @@ package system
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"text/template" "text/template"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/builder"
"github.com/docker/cli/cli/command/container" "github.com/docker/cli/cli/command/container"
"github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/command/network" "github.com/docker/cli/cli/command/network"
@@ -21,20 +21,21 @@ import (
type pruneOptions struct { type pruneOptions struct {
force bool force bool
all bool all bool
pruneBuildCache bool
pruneVolumes bool pruneVolumes bool
pruneBuildCache bool
filter opts.FilterOpt filter opts.FilterOpt
} }
// newPruneCommand creates a new cobra.Command for `docker prune` // newPruneCommand creates a new cobra.Command for `docker prune`
func newPruneCommand(dockerCli command.Cli) *cobra.Command { func newPruneCommand(dockerCli command.Cli) *cobra.Command {
options := pruneOptions{filter: opts.NewFilterOpt(), pruneBuildCache: true} options := pruneOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "prune [OPTIONS]", Use: "prune [OPTIONS]",
Short: "Remove unused data", Short: "Remove unused data",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
options.pruneBuildCache = versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.31")
return runPrune(dockerCli, options) return runPrune(dockerCli, options)
}, },
Annotations: map[string]string{"version": "1.25"}, Annotations: map[string]string{"version": "1.25"},
@@ -57,44 +58,30 @@ const confirmationTemplate = `WARNING! This will remove:
{{- end }} {{- end }}
Are you sure you want to continue?` Are you sure you want to continue?`
// runBuildCachePrune executes a prune command for build cache
func runBuildCachePrune(dockerCli command.Cli, _ opts.FilterOpt) (uint64, string, error) {
report, err := dockerCli.Client().BuildCachePrune(context.Background())
if err != nil {
return 0, "", err
}
return report.SpaceReclaimed, "", nil
}
func runPrune(dockerCli command.Cli, options pruneOptions) error { func runPrune(dockerCli command.Cli, options pruneOptions) error {
// TODO version this once "until" filter is supported for volumes // TODO version this once "until" filter is supported for volumes
if options.pruneVolumes && options.filter.Value().Contains("until") { if options.pruneVolumes && options.filter.Value().Contains("until") {
return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`) return fmt.Errorf(`ERROR: The "until" filter is not supported with "--volumes"`)
} }
if versions.LessThan(dockerCli.Client().ClientVersion(), "1.31") {
options.pruneBuildCache = false
}
if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), confirmationMessage(options)) { if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), confirmationMessage(options)) {
return nil return nil
} }
imagePrune := func(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) { pruneFuncs := []func(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error){
return image.RunPrune(dockerCli, options.all, options.filter)
}
pruneFuncs := []func(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error){
container.RunPrune, container.RunPrune,
network.RunPrune, network.RunPrune,
} }
if options.pruneVolumes { if options.pruneVolumes {
pruneFuncs = append(pruneFuncs, volume.RunPrune) pruneFuncs = append(pruneFuncs, volume.RunPrune)
} }
pruneFuncs = append(pruneFuncs, imagePrune)
if options.pruneBuildCache { if options.pruneBuildCache {
pruneFuncs = append(pruneFuncs, runBuildCachePrune) pruneFuncs = append(pruneFuncs, builder.CachePrune)
} }
// FIXME: modify image.RunPrune to not modify options.filter, otherwise this has to be last in the list.
pruneFuncs = append(pruneFuncs, image.RunPrune)
var spaceReclaimed uint64 var spaceReclaimed uint64
for _, pruneFn := range pruneFuncs { for _, pruneFn := range pruneFuncs {
spc, output, err := pruneFn(dockerCli, options.filter) spc, output, err := pruneFn(dockerCli, options.all, options.filter)
if err != nil { if err != nil {
return err return err
} }
@@ -126,7 +113,11 @@ func confirmationMessage(options pruneOptions) string {
warnings = append(warnings, "all dangling images") warnings = append(warnings, "all dangling images")
} }
if options.pruneBuildCache { if options.pruneBuildCache {
warnings = append(warnings, "all build cache") if options.all {
warnings = append(warnings, "all build cache")
} else {
warnings = append(warnings, "all dangling build cache")
}
} }
if len(options.filter.String()) > 0 { if len(options.filter.String()) > 0 {
warnings = append(warnings, "Elements to be pruned will be filtered with:") warnings = append(warnings, "Elements to be pruned will be filtered with:")
+1 -1
View File
@@ -148,7 +148,7 @@ func TestAddStageSigners(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
changeList := cl.List() changeList := cl.List()
assert.Check(t, is.Len(changeList, 4)) assert.Check(t, is.Len(changeList, 4))
// ordering is determinstic: // ordering is deterministic:
// first change is for targets/user key creation // first change is for targets/user key creation
newSignerKeyChange := changeList[0] newSignerKeyChange := changeList[0]
+1 -1
View File
@@ -73,6 +73,6 @@ func runPrune(dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint6
// RunPrune calls the Volume Prune API // RunPrune calls the Volume Prune API
// This returns the amount of space reclaimed and a detailed output string // This returns the amount of space reclaimed and a detailed output string
func RunPrune(dockerCli command.Cli, filter opts.FilterOpt) (uint64, string, error) { func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) return runPrune(dockerCli, pruneOptions{force: true, filter: filter})
} }
+11 -5
View File
@@ -176,15 +176,21 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]extractedValu
// Soft default (fall back if unset or empty) // Soft default (fall back if unset or empty)
func softDefault(substitution string, mapping Mapping) (string, bool, error) { func softDefault(substitution string, mapping Mapping) (string, bool, error) {
return withDefault(substitution, mapping, "-:") sep := ":-"
if !strings.Contains(substitution, sep) {
return "", false, nil
}
name, defaultValue := partition(substitution, sep)
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue, true, nil
}
return value, true, nil
} }
// Hard default (fall back if-and-only-if empty) // Hard default (fall back if-and-only-if empty)
func hardDefault(substitution string, mapping Mapping) (string, bool, error) { func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
return withDefault(substitution, mapping, "-") sep := "-"
}
func withDefault(substitution string, mapping Mapping, sep string) (string, bool, error) {
if !strings.Contains(substitution, sep) { if !strings.Contains(substitution, sep) {
return "", false, nil return "", false, nil
} }
+6
View File
@@ -78,6 +78,12 @@ func TestEmptyValueWithSoftDefault(t *testing.T) {
assert.Check(t, is.Equal("ok def", result)) assert.Check(t, is.Equal("ok def", result))
} }
func TestValueWithSoftDefault(t *testing.T) {
result, err := Substitute("ok ${FOO:-def}", defaultMapping)
assert.NilError(t, err)
assert.Check(t, is.Equal("ok first", result))
}
func TestEmptyValueWithHardDefault(t *testing.T) { func TestEmptyValueWithHardDefault(t *testing.T) {
result, err := Substitute("ok ${BAR-def}", defaultMapping) result, err := Substitute("ok ${BAR-def}", defaultMapping)
assert.NilError(t, err) assert.NilError(t, err)
+76 -13
View File
@@ -10,8 +10,10 @@ import (
"net/url" "net/url"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/docker/cli/cli/connhelper/ssh" "github.com/docker/cli/cli/connhelper/ssh"
@@ -82,6 +84,9 @@ func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn,
// commandConn implements net.Conn // commandConn implements net.Conn
type commandConn struct { type commandConn struct {
cmd *exec.Cmd cmd *exec.Cmd
cmdExited bool
cmdWaitErr error
cmdMutex sync.Mutex
stdin io.WriteCloser stdin io.WriteCloser
stdout io.ReadCloser stdout io.ReadCloser
stderrMu sync.Mutex stderrMu sync.Mutex
@@ -101,23 +106,75 @@ func (c *commandConn) killIfStdioClosed() error {
if !stdioClosed { if !stdioClosed {
return nil return nil
} }
var err error return c.kill()
// NOTE: maybe already killed here }
if err = c.cmd.Process.Kill(); err == nil {
err = c.cmd.Wait() // killAndWait tries sending SIGTERM to the process before sending SIGKILL.
func killAndWait(cmd *exec.Cmd) error {
var werr error
if runtime.GOOS != "windows" {
werrCh := make(chan error)
go func() { werrCh <- cmd.Wait() }()
cmd.Process.Signal(syscall.SIGTERM)
select {
case werr = <-werrCh:
case <-time.After(3 * time.Second):
cmd.Process.Kill()
werr = <-werrCh
}
} else {
cmd.Process.Kill()
werr = cmd.Wait()
} }
if err != nil { return werr
// err is typically "os: process already finished". }
// we check ProcessState here instead of `strings.Contains(err, "os: process already finished")`
if c.cmd.ProcessState.Exited() { // kill returns nil if the command terminated, regardless to the exit status.
err = nil func (c *commandConn) kill() error {
var werr error
c.cmdMutex.Lock()
if c.cmdExited {
werr = c.cmdWaitErr
} else {
werr = killAndWait(c.cmd)
c.cmdWaitErr = werr
c.cmdExited = true
}
c.cmdMutex.Unlock()
if werr == nil {
return nil
}
wExitErr, ok := werr.(*exec.ExitError)
if ok {
if wExitErr.ProcessState.Exited() {
return nil
} }
} }
return err return errors.Wrapf(werr, "connhelper: failed to wait")
} }
func (c *commandConn) onEOF(eof error) error { func (c *commandConn) onEOF(eof error) error {
werr := c.cmd.Wait() // when we got EOF, the command is going to be terminated
var werr error
c.cmdMutex.Lock()
if c.cmdExited {
werr = c.cmdWaitErr
} else {
werrCh := make(chan error)
go func() { werrCh <- c.cmd.Wait() }()
select {
case werr = <-werrCh:
c.cmdWaitErr = werr
c.cmdExited = true
case <-time.After(10 * time.Second):
c.cmdMutex.Unlock()
c.stderrMu.Lock()
stderr := c.stderr.String()
c.stderrMu.Unlock()
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr)
}
}
c.cmdMutex.Unlock()
if werr == nil { if werr == nil {
return eof return eof
} }
@@ -148,7 +205,10 @@ func (c *commandConn) CloseRead() error {
c.stdioClosedMu.Lock() c.stdioClosedMu.Lock()
c.stdoutClosed = true c.stdoutClosed = true
c.stdioClosedMu.Unlock() c.stdioClosedMu.Unlock()
return c.killIfStdioClosed() if err := c.killIfStdioClosed(); err != nil {
logrus.Warnf("commandConn.CloseRead: %v", err)
}
return nil
} }
func (c *commandConn) Read(p []byte) (int, error) { func (c *commandConn) Read(p []byte) (int, error) {
@@ -167,7 +227,10 @@ func (c *commandConn) CloseWrite() error {
c.stdioClosedMu.Lock() c.stdioClosedMu.Lock()
c.stdinClosed = true c.stdinClosed = true
c.stdioClosedMu.Unlock() c.stdioClosedMu.Unlock()
return c.killIfStdioClosed() if err := c.killIfStdioClosed(); err != nil {
logrus.Warnf("commandConn.CloseWrite: %v", err)
}
return nil
} }
func (c *commandConn) Write(p []byte) (int, error) { func (c *commandConn) Write(p []byte) (int, error) {
+9 -3
View File
@@ -200,7 +200,7 @@ func continueOnError(err error) bool {
} }
func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error { func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error {
endpoints, err := allEndpoints(namedRef) endpoints, err := allEndpoints(namedRef, c.insecureRegistry)
if err != nil { if err != nil {
return err return err
} }
@@ -262,12 +262,18 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
} }
// allEndpoints returns a list of endpoints ordered by priority (v2, https, v1). // allEndpoints returns a list of endpoints ordered by priority (v2, https, v1).
func allEndpoints(namedRef reference.Named) ([]registry.APIEndpoint, error) { func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) {
repoInfo, err := registry.ParseRepositoryInfo(namedRef) repoInfo, err := registry.ParseRepositoryInfo(namedRef)
if err != nil { if err != nil {
return nil, err return nil, err
} }
registryService, err := registry.NewService(registry.ServiceOptions{})
var serviceOpts registry.ServiceOptions
if insecure {
logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef))
serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)}
}
registryService, err := registry.NewService(serviceOpts)
if err != nil { if err != nil {
return []registry.APIEndpoint{}, err return []registry.APIEndpoint{}, err
} }
+20 -4
View File
@@ -13,6 +13,7 @@ import (
cliconfig "github.com/docker/cli/cli/config" cliconfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/cli/internal/containerizedengine"
"github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/term" "github.com/docker/docker/pkg/term"
@@ -32,6 +33,9 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
SilenceErrors: true, SilenceErrors: true,
TraverseChildren: true, TraverseChildren: true,
Args: noArgs, Args: noArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return command.ShowHelp(dockerCli.Err())(cmd, args)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// flags must be the top-level command flags, not cmd.Flags() // flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags) opts.Common.SetDefaultOptions(flags)
@@ -99,8 +103,10 @@ func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.
ccmd.Println(err) ccmd.Println(err)
return return
} }
if err := hideUnsupportedFeatures(ccmd, dockerCli); err != nil {
hideUnsupportedFeatures(ccmd, dockerCli) ccmd.Println(err)
return
}
defaultHelpFunc(ccmd, args) defaultHelpFunc(ccmd, args)
}) })
} }
@@ -168,7 +174,7 @@ func main() {
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
logrus.SetOutput(stderr) logrus.SetOutput(stderr)
dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled()) dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled(), containerizedengine.NewClient)
cmd := newDockerCommand(dockerCli) cmd := newDockerCommand(dockerCli)
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {
@@ -234,15 +240,21 @@ func hideFeatureSubCommand(subcmd *cobra.Command, hasFeature bool, annotation st
} }
} }
func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error {
clientVersion := details.Client().ClientVersion() clientVersion := details.Client().ClientVersion()
osType := details.ServerInfo().OSType osType := details.ServerInfo().OSType
hasExperimental := details.ServerInfo().HasExperimental hasExperimental := details.ServerInfo().HasExperimental
hasExperimentalCLI := details.ClientInfo().HasExperimental hasExperimentalCLI := details.ClientInfo().HasExperimental
hasBuildKit, err := command.BuildKitEnabled(details.ServerInfo())
if err != nil {
return err
}
cmd.Flags().VisitAll(func(f *pflag.Flag) { cmd.Flags().VisitAll(func(f *pflag.Flag) {
hideFeatureFlag(f, hasExperimental, "experimental") hideFeatureFlag(f, hasExperimental, "experimental")
hideFeatureFlag(f, hasExperimentalCLI, "experimentalCLI") hideFeatureFlag(f, hasExperimentalCLI, "experimentalCLI")
hideFeatureFlag(f, hasBuildKit, "buildkit")
hideFeatureFlag(f, !hasBuildKit, "no-buildkit")
// hide flags not supported by the server // hide flags not supported by the server
if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) { if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
f.Hidden = true f.Hidden = true
@@ -258,6 +270,8 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
for _, subcmd := range cmd.Commands() { for _, subcmd := range cmd.Commands() {
hideFeatureSubCommand(subcmd, hasExperimental, "experimental") hideFeatureSubCommand(subcmd, hasExperimental, "experimental")
hideFeatureSubCommand(subcmd, hasExperimentalCLI, "experimentalCLI") hideFeatureSubCommand(subcmd, hasExperimentalCLI, "experimentalCLI")
hideFeatureSubCommand(subcmd, hasBuildKit, "buildkit")
hideFeatureSubCommand(subcmd, !hasBuildKit, "no-buildkit")
// hide subcommands not supported by the server // hide subcommands not supported by the server
if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) { if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
subcmd.Hidden = true subcmd.Hidden = true
@@ -266,6 +280,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
subcmd.Hidden = true subcmd.Hidden = true
} }
} }
return nil
} }
// Checks if a command or one of its ancestors is in the list // Checks if a command or one of its ancestors is in the list
@@ -312,6 +327,7 @@ func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
errs = append(errs, fmt.Sprintf("\"--%s\" is on a Docker cli with experimental cli features enabled", f.Name)) errs = append(errs, fmt.Sprintf("\"--%s\" is on a Docker cli with experimental cli features enabled", f.Name))
} }
// buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case
} }
}) })
if len(errs) > 0 { if len(errs) > 0 {
+19 -1
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"testing" "testing"
@@ -26,8 +27,25 @@ func TestClientDebugEnabled(t *testing.T) {
func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) { func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) {
discard := ioutil.Discard discard := ioutil.Discard
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false)) cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil))
cmd.SetArgs([]string{"help", "invalid"}) cmd.SetArgs([]string{"help", "invalid"})
err := cmd.Execute() err := cmd.Execute()
assert.Error(t, err, "unknown help topic: invalid") assert.Error(t, err, "unknown help topic: invalid")
} }
func TestExitStatusForInvalidSubcommand(t *testing.T) {
discard := ioutil.Discard
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil))
cmd.SetArgs([]string{"invalid"})
err := cmd.Execute()
assert.Check(t, is.ErrorContains(err, "docker: 'invalid' is not a docker command."))
}
func TestVersion(t *testing.T) {
var b bytes.Buffer
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, &b, &b, false, nil))
cmd.SetArgs([]string{"--version"})
err := cmd.Execute()
assert.NilError(t, err)
assert.Check(t, is.Contains(b.String(), "Docker version"))
}
+1 -9
View File
@@ -105,7 +105,7 @@ shellcheck: build_shell_validate_image ## run shellcheck validation
docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck
.PHONY: test-e2e ## run e2e tests .PHONY: test-e2e ## run e2e tests
test-e2e: test-e2e-non-experimental test-e2e-experimental test-e2e-containerized test-e2e: test-e2e-non-experimental test-e2e-experimental
.PHONY: test-e2e-experimental .PHONY: test-e2e-experimental
test-e2e-experimental: build_e2e_image test-e2e-experimental: build_e2e_image
@@ -115,14 +115,6 @@ test-e2e-experimental: build_e2e_image
test-e2e-non-experimental: build_e2e_image test-e2e-non-experimental: build_e2e_image
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock $(E2E_IMAGE_NAME) docker run --rm -v /var/run/docker.sock:/var/run/docker.sock $(E2E_IMAGE_NAME)
.PHONY: test-e2e-containerized
test-e2e-containerized: build_e2e_image
docker run --rm --privileged \
-v /var/lib/docker \
-v /var/lib/containerd \
-v /lib/modules:/lib/modules \
$(E2E_IMAGE_NAME) /go/src/github.com/docker/cli/scripts/test/engine/entry
.PHONY: help .PHONY: help
help: ## print this help help: ## print this help
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.10.3-alpine FROM golang:1.10.4-alpine
RUN apk add -U git bash coreutils gcc musl-dev RUN apk add -U git bash coreutils gcc musl-dev
+1 -1
View File
@@ -1,3 +1,3 @@
FROM dockercore/golang-cross:1.10.3@sha256:7671b4ed357fda50124e5679d36c4c3206ded4d43f1d2e0ff3d120a1e2bf94d7 FROM dockercore/golang-cross:1.10.4@sha256:55c7b933ac944f4922b673b4d4340d1a0404f3c324bd0b3f13a4326c427b1f2a
ENV DISABLE_WARN_OUTSIDE_CONTAINER=1 ENV DISABLE_WARN_OUTSIDE_CONTAINER=1
WORKDIR /go/src/github.com/docker/cli WORKDIR /go/src/github.com/docker/cli
+1 -1
View File
@@ -1,5 +1,5 @@
FROM golang:1.10.3-alpine FROM golang:1.10.4-alpine
RUN apk add -U git make bash coreutils ca-certificates curl RUN apk add -U git make bash coreutils ca-certificates curl
+1 -23
View File
@@ -1,4 +1,4 @@
ARG GO_VERSION=1.10.3 ARG GO_VERSION=1.10.4
FROM docker/containerd-shim-process:a4d1531 AS containerd-shim-process FROM docker/containerd-shim-process:a4d1531 AS containerd-shim-process
@@ -15,28 +15,6 @@ RUN apt-get update && apt-get install -y \
iptables \ iptables \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# TODO - consider replacing with an official image and a multi-stage build to pluck the binaries out
#ARG CONTAINERD_VERSION=v1.1.2
#ARG CONTAINERD_VERSION=47a128d
#ARG CONTAINERD_VERSION=6c3e782f
ARG CONTAINERD_VERSION=65839a47a88b0a1c5dc34981f1741eccefc9f2b0
RUN git clone https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd && \
cd /go/src/github.com/containerd/containerd && \
git checkout ${CONTAINERD_VERSION} && \
make && \
make install
COPY e2eengine/config.toml /etc/containerd/config.toml
COPY --from=containerd-shim-process /bin/containerd-shim-process-v1 /bin/
# TODO - consider replacing with an official image and a multi-stage build to pluck the binaries out
ARG RUNC_VERSION=v1.0.0-rc5
RUN git clone https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc && \
cd /go/src/github.com/opencontainers/runc && \
git checkout ${RUNC_VERSION} && \
make && \
make install
ARG COMPOSE_VERSION=1.21.2 ARG COMPOSE_VERSION=1.21.2
RUN curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose \ RUN curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose \
&& chmod +x /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.10.3-alpine FROM golang:1.10.4-alpine
RUN apk add -U git RUN apk add -U git
+32 -1
View File
@@ -19,6 +19,37 @@ The following list of features are deprecated in Engine.
To learn more about Docker Engine's deprecation policy, To learn more about Docker Engine's deprecation policy,
see [Feature Deprecation Policy](https://docs.docker.com/engine/#feature-deprecation-policy). see [Feature Deprecation Policy](https://docs.docker.com/engine/#feature-deprecation-policy).
### Legacy "overlay" storage driver
**Deprecated in Release: v18.09.0**
The `overlay` storage driver is deprecated in favor of the `overlay2` storage
driver, which has all the benefits of `overlay`, without its limitations (excessive
inode consumption). The legacy `overlay` storage driver will be removed in a future
release. Users of the `overlay` storage driver should migrate to the `overlay2`
storage driver.
The legacy `overlay` storage driver allowed using overlayFS-backed filesystems
on pre 4.x kernels. Now that all supported distributions are able to run `overlay2`
(as they are either on kernel 4.x, or have support for multiple lowerdirs
backported), there is no reason to keep maintaining the `overlay` storage driver.
### device mapper storage driver
**Deprecated in Release: v18.09.0**
The `devicemapper` storage driver is deprecated in favor of `overlay2`, and will
be removed in a future release. Users of the `devicemapper` storage driver are
recommended to migrate to a different storage driver, such as `overlay2`, which
is now the default storage driver.
The `devicemapper` storage driver facilitates running Docker on older (3.x) kernels
that have no support for other storage drivers (such as overlay2, or AUFS).
Now that support for `overlay2` is added to all supported distros (as they are
either on kernel 4.x, or have support for multiple lowerdirs backported), there
is no reason to continue maintenance of the `devicemapper` storage driver.
### Reserved namespaces in engine labels ### Reserved namespaces in engine labels
**Deprecated in Release: v18.06.0** **Deprecated in Release: v18.06.0**
@@ -167,7 +198,7 @@ The docker login command is removing the ability to automatically register for a
**Target For Removal In Release: v17.06** **Target For Removal In Release: v17.06**
The flag `--security-opt` doesn't use the colon separator(`:`) anymore to divide keys and values, it uses the equal symbol(`=`) for consistency with other similar flags, like `--storage-opt`. The flag `--security-opt` doesn't use the colon separator (`:`) anymore to divide keys and values, it uses the equal symbol (`=`) for consistency with other similar flags, like `--storage-opt`.
### `/containers/(id or name)/copy` endpoint ### `/containers/(id or name)/copy` endpoint
+1 -1
View File
@@ -1379,7 +1379,7 @@ Keep the following things in mind about volumes in the `Dockerfile`.
data within the volume after it has been declared, those changes will be discarded. data within the volume after it has been declared, those changes will be discarded.
- **JSON formatting**: The list is parsed as a JSON array. - **JSON formatting**: The list is parsed as a JSON array.
You must enclose words with double quotes (`"`)rather than single quotes (`'`). You must enclose words with double quotes (`"`) rather than single quotes (`'`).
- **The host directory is declared at container run-time**: The host directory - **The host directory is declared at container run-time**: The host directory
(the mountpoint) is, by its nature, host-dependent. This is to preserve image (the mountpoint) is, by its nature, host-dependent. This is to preserve image
+4
View File
@@ -48,14 +48,18 @@ Options:
'<network-name>|<network-id>': connect to a user-defined network '<network-name>|<network-id>': connect to a user-defined network
--no-cache Do not use cache when building the image --no-cache Do not use cache when building the image
--pull Always attempt to pull a newer version of the image --pull Always attempt to pull a newer version of the image
--progress Set type of progress output (only if BuildKit enabled) (auto, plain, tty).
Use plain to show container output
-q, --quiet Suppress the build output and print image ID on success -q, --quiet Suppress the build output and print image ID on success
--rm Remove intermediate containers after a successful build (default true) --rm Remove intermediate containers after a successful build (default true)
--secret Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret"
--security-opt value Security Options (default []) --security-opt value Security Options (default [])
--shm-size bytes Size of /dev/shm --shm-size bytes Size of /dev/shm
The format is `<number><unit>`. `number` must be greater than `0`. The format is `<number><unit>`. `number` must be greater than `0`.
Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes),
or `g` (gigabytes). If you omit the unit, the system uses bytes. or `g` (gigabytes). If you omit the unit, the system uses bytes.
--squash Squash newly built layers into a single new layer (**Experimental Only**) --squash Squash newly built layers into a single new layer (**Experimental Only**)
--ssh SSH agent socket or keys to expose to the build (only if BuildKit enabled) (format: default|<id>[=<socket>|<key>[,<key>]])
-t, --tag value Name and optionally a tag in the 'name:tag' format (default []) -t, --tag value Name and optionally a tag in the 'name:tag' format (default [])
--target string Set the target build stage to build. --target string Set the target build stage to build.
--ulimit value Ulimit options (default []) --ulimit value Ulimit options (default [])
+2 -2
View File
@@ -85,7 +85,7 @@ Options:
--memory-reservation string Memory soft limit --memory-reservation string Memory soft limit
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap --memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--memory-swappiness int Tune container memory swappiness (0 to 100) (default -1) --memory-swappiness int Tune container memory swappiness (0 to 100) (default -1)
--mount value Attach a filesytem mount to the container (default []) --mount value Attach a filesystem mount to the container (default [])
--name string Assign a name to the container --name string Assign a name to the container
--network-alias value Add network-scoped alias for the container (default []) --network-alias value Add network-scoped alias for the container (default [])
--network string Connect a container to a network (default "default") --network string Connect a container to a network (default "default")
@@ -256,5 +256,5 @@ docker create --device-cgroup-rule='c 42:* rmw' -name my-container my-image
Then, a user could ask `udev` to execute a script that would `docker exec my-container mknod newDevX c 42 <minor>` Then, a user could ask `udev` to execute a script that would `docker exec my-container mknod newDevX c 42 <minor>`
the required device when it is added. the required device when it is added.
NOTE: initially present devices still need to be explicitely added to NOTE: initially present devices still need to be explicitly added to
the create/run command the create/run command
+25 -3
View File
@@ -191,7 +191,10 @@ $ docker -H ssh://example.com ps
``` ```
To use SSH connection, you need to set up `ssh` so that it can reach the To use SSH connection, you need to set up `ssh` so that it can reach the
remote host with public key authentication. remote host with public key authentication. Password authentication is not
supported. If your key is protected with passphrase, you need to set up
`ssh-agent`.
Also, you need to have `docker` binary 18.09 or later on the daemon host. Also, you need to have `docker` binary 18.09 or later on the daemon host.
#### Bind Docker to another host/port or a Unix socket #### Bind Docker to another host/port or a Unix socket
@@ -1297,6 +1300,7 @@ This is a full example of the allowed configuration options on Linux:
"exec-opts": [], "exec-opts": [],
"exec-root": "", "exec-root": "",
"experimental": false, "experimental": false,
"features": {},
"storage-driver": "", "storage-driver": "",
"storage-opts": [], "storage-opts": [],
"labels": [], "labels": [],
@@ -1326,7 +1330,13 @@ This is a full example of the allowed configuration options on Linux:
"userns-remap": "", "userns-remap": "",
"group": "", "group": "",
"cgroup-parent": "", "cgroup-parent": "",
"default-ulimits": {}, "default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
}
},
"init": false, "init": false,
"init-path": "/usr/libexec/docker-init", "init-path": "/usr/libexec/docker-init",
"ipv6": false, "ipv6": false,
@@ -1392,6 +1402,7 @@ This is a full example of the allowed configuration options on Windows:
"dns-search": [], "dns-search": [],
"exec-opts": [], "exec-opts": [],
"experimental": false, "experimental": false,
"features":{},
"storage-driver": "", "storage-driver": "",
"storage-opts": [], "storage-opts": [],
"labels": [], "labels": [],
@@ -1422,6 +1433,16 @@ This is a full example of the allowed configuration options on Windows:
} }
``` ```
#### Feature options
The optional field `features` in `daemon.json` allows users to enable or disable specific
daemon features. For example, `{"features":{"buildkit": true}}` enables `buildkit` as the
default docker image builder.
The list of currently supported feature options:
- `buildkit`: It enables `buildkit` as default builder when set to `true` or disables it by
`false`. Note that if this option is not explicitly set in the daemon config file, then it
is up to the cli to determine which builder to invoke.
#### Configuration reload behavior #### Configuration reload behavior
Some options can be reconfigured when the daemon is running without requiring Some options can be reconfigured when the daemon is running without requiring
@@ -1446,11 +1467,12 @@ The list of currently supported options that can be reconfigured is this:
the runtime shipped with the official docker packages. the runtime shipped with the official docker packages.
- `runtimes`: it updates the list of available OCI runtimes that can - `runtimes`: it updates the list of available OCI runtimes that can
be used to run containers. be used to run containers.
- `authorization-plugin`: specifies the authorization plugins to use. - `authorization-plugin`: it specifies the authorization plugins to use.
- `allow-nondistributable-artifacts`: Replaces the set of registries to which the daemon will push nondistributable artifacts with a new set of registries. - `allow-nondistributable-artifacts`: Replaces the set of registries to which the daemon will push nondistributable artifacts with a new set of registries.
- `insecure-registries`: it replaces the daemon insecure registries with a new set of insecure registries. If some existing insecure registries in daemon's configuration are not in newly reloaded insecure resgitries, these existing ones will be removed from daemon's config. - `insecure-registries`: it replaces the daemon insecure registries with a new set of insecure registries. If some existing insecure registries in daemon's configuration are not in newly reloaded insecure resgitries, these existing ones will be removed from daemon's config.
- `registry-mirrors`: it replaces the daemon registry mirrors with a new set of registry mirrors. If some existing registry mirrors in daemon's configuration are not in newly reloaded registry mirrors, these existing ones will be removed from daemon's config. - `registry-mirrors`: it replaces the daemon registry mirrors with a new set of registry mirrors. If some existing registry mirrors in daemon's configuration are not in newly reloaded registry mirrors, these existing ones will be removed from daemon's config.
- `shutdown-timeout`: it replaces the daemon's existing configuration timeout with a new timeout for shutting down all containers. - `shutdown-timeout`: it replaces the daemon's existing configuration timeout with a new timeout for shutting down all containers.
- `features`: it explicitly enables or disables specific features.
Updating and reloading the cluster configurations such as `--cluster-store`, Updating and reloading the cluster configurations such as `--cluster-store`,
`--cluster-advertise` and `--cluster-store-opts` will take effect only if `--cluster-advertise` and `--cluster-store-opts` will take effect only if
+9
View File
@@ -24,6 +24,7 @@ Options:
-c, --change value Apply Dockerfile instruction to the created image (default []) -c, --change value Apply Dockerfile instruction to the created image (default [])
--help Print usage --help Print usage
-m, --message string Set commit message for imported image -m, --message string Set commit message for imported image
--platform string Set platform if server is multi-platform capable
``` ```
## Description ## Description
@@ -87,3 +88,11 @@ Note the `sudo` in this example – you must preserve
the ownership of the files (especially root ownership) during the the ownership of the files (especially root ownership) during the
archiving with tar. If you are not root (or the sudo command) when you archiving with tar. If you are not root (or the sudo command) when you
tar, then the ownerships might not get preserved. tar, then the ownerships might not get preserved.
## When the daemon supports multiple operating systems
If the daemon supports multiple operating systems, and the image being imported
does not match the default operating system, it may be necessary to add
`--platform`. This would be necessary when importing a Linux image into a Windows
daemon.
# docker import --platform=linux .\linuximage.tar
+9 -3
View File
@@ -85,6 +85,8 @@ you can download them from:
- Microsoft Windows Credential Manager: https://github.com/docker/docker-credential-helpers/releases - Microsoft Windows Credential Manager: https://github.com/docker/docker-credential-helpers/releases
- [pass](https://www.passwordstore.org/): https://github.com/docker/docker-credential-helpers/releases - [pass](https://www.passwordstore.org/): https://github.com/docker/docker-credential-helpers/releases
#### Configure the credentials store
You need to specify the credentials store in `$HOME/.docker/config.json` You need to specify the credentials store in `$HOME/.docker/config.json`
to tell the docker engine to use it. The value of the config property should be to tell the docker engine to use it. The value of the config property should be
the suffix of the program to use (i.e. everything after `docker-credential-`). the suffix of the program to use (i.e. everything after `docker-credential-`).
@@ -99,7 +101,7 @@ For example, to use `docker-credential-osxkeychain`:
If you are currently logged in, run `docker logout` to remove If you are currently logged in, run `docker logout` to remove
the credentials from the file and run `docker login` again. the credentials from the file and run `docker login` again.
### Default behavior #### Default behavior
By default, Docker looks for the native binary on each of the platforms, i.e. By default, Docker looks for the native binary on each of the platforms, i.e.
"osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special "osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special
@@ -108,7 +110,7 @@ it cannot find the "pass" binary. If none of these binaries are present, it
stores the credentials (i.e. password) in base64 encoding in the config files stores the credentials (i.e. password) in base64 encoding in the config files
described above. described above.
### Credential helper protocol #### Credential helper protocol
Credential helpers can be any program or script that follows a very simple protocol. Credential helpers can be any program or script that follows a very simple protocol.
This protocol is heavily inspired by Git, but it differs in the information shared. This protocol is heavily inspired by Git, but it differs in the information shared.
@@ -162,7 +164,7 @@ designated programs to handle credentials for *specific registries*. The default
credential store (`credsStore` or the config file itself) will not be used for credential store (`credsStore` or the config file itself) will not be used for
operations concerning credentials of the specified registries. operations concerning credentials of the specified registries.
### Logging out #### Configure credential helpers
If you are currently logged in, run `docker logout` to remove If you are currently logged in, run `docker logout` to remove
the credentials from the default store. the credentials from the default store.
@@ -182,3 +184,7 @@ For example:
} }
} }
``` ```
## Related commands
* [logout](logout.md)
+4
View File
@@ -30,3 +30,7 @@ Options:
```bash ```bash
$ docker logout localhost:8080 $ docker logout localhost:8080
``` ```
## Related commands
* [login](login.md)
+3 -3
View File
@@ -46,11 +46,11 @@ $ docker rmi fd484f19954f
Error: Conflict, cannot delete image fd484f19954f because it is tagged in multiple repositories, use -f to force Error: Conflict, cannot delete image fd484f19954f because it is tagged in multiple repositories, use -f to force
2013/12/11 05:47:16 Error: failed to remove one or more images 2013/12/11 05:47:16 Error: failed to remove one or more images
$ docker rmi test1 $ docker rmi test1:latest
Untagged: test1:latest Untagged: test1:latest
$ docker rmi test2 $ docker rmi test2:latest
Untagged: test2:latest Untagged: test2:latest
@@ -60,7 +60,7 @@ $ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE REPOSITORY TAG IMAGE ID CREATED SIZE
test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB) test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
$ docker rmi test $ docker rmi test:latest
Untagged: test:latest Untagged: test:latest
Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8 Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8
+1 -1
View File
@@ -418,7 +418,7 @@ $ docker run -l my-label --label com.example.foo=bar ubuntu bash
``` ```
The `my-label` key doesn't specify a value so the label defaults to an empty The `my-label` key doesn't specify a value so the label defaults to an empty
string(`""`). To add multiple labels, repeat the label flag (`-l` or `--label`). string (`""`). To add multiple labels, repeat the label flag (`-l` or `--label`).
The `key=value` must be unique to avoid overwriting the label value. If you The `key=value` must be unique to avoid overwriting the label value. If you
specify labels with identical keys but different values, each subsequent value specify labels with identical keys but different values, each subsequent value
+2 -2
View File
@@ -1085,7 +1085,7 @@ per second from `/dev/sda`:
$ docker run -it --device-read-bps /dev/sda:1mb ubuntu $ docker run -it --device-read-bps /dev/sda:1mb ubuntu
The `--device-write-bps` flag limits the write rate (bytes per second)to a device. The `--device-write-bps` flag limits the write rate (bytes per second) to a device.
For example, this command creates a container and limits the write rate to `1mb` For example, this command creates a container and limits the write rate to `1mb`
per second for `/dev/sda`: per second for `/dev/sda`:
@@ -1555,7 +1555,7 @@ The example below mounts an empty tmpfs into the container with the `rw`,
If neither 'rw' or 'ro' is specified then the volume is mounted in If neither 'rw' or 'ro' is specified then the volume is mounted in
read-write mode. read-write mode.
The `nocopy` modes is used to disable automatic copying requested volume The `nocopy` mode is used to disable automatically copying the requested volume
path in the container to the volume storage location. path in the container to the volume storage location.
For named volumes, `copy` is the default mode. Copy modes are not supported For named volumes, `copy` is the default mode. Copy modes are not supported
for bind-mounted volumes. for bind-mounted volumes.
+1 -1
View File
@@ -19,7 +19,7 @@ const descriptionSourcePath = "docs/reference/commandline/"
func generateCliYaml(opts *options) error { func generateCliYaml(opts *options) error {
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false) dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil)
cmd := &cobra.Command{Use: "docker"} cmd := &cobra.Command{Use: "docker"}
commands.AddCommands(cmd, dockerCli) commands.AddCommands(cmd, dockerCli)
source := filepath.Join(opts.source, descriptionSourcePath) source := filepath.Join(opts.source, descriptionSourcePath)
-42
View File
@@ -1,42 +0,0 @@
package check
import (
"os"
"testing"
"github.com/docker/cli/e2eengine"
"gotest.tools/icmd"
)
func TestDockerEngineOnContainerdAltRootConfig(t *testing.T) {
defer func() {
err := e2eengine.CleanupEngine(t)
if err != nil {
t.Errorf("Failed to cleanup engine: %s", err)
}
}()
t.Log("First engine init")
// First init
result := icmd.RunCmd(icmd.Command("docker", "engine", "init", "--config-file", "/tmp/etc/docker/daemon.json"),
func(c *icmd.Cmd) {
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
})
result.Assert(t, icmd.Expected{
Out: "Success! The docker engine is now running.",
Err: "",
ExitCode: 0,
})
// Make sure update doesn't blow up with alternate config path
t.Log("perform update")
// Now update and succeed
targetVersion := os.Getenv("VERSION")
result = icmd.RunCmd(icmd.Command("docker", "engine", "update", "--version", targetVersion))
result.Assert(t, icmd.Expected{
Out: "Success! The docker engine is now running.",
Err: "",
ExitCode: 0,
})
}
-14
View File
@@ -1,14 +0,0 @@
root = "/var/lib/containerd"
state = "/run/containerd"
oom_score = 0
[grpc]
address = "/run/containerd/containerd.sock"
uid = 0
gid = 0
[debug]
address = "/run/containerd/debug.sock"
uid = 0
gid = 0
level = "debug"
-85
View File
@@ -1,85 +0,0 @@
package multi
import (
"os"
"testing"
"github.com/docker/cli/e2eengine"
"gotest.tools/icmd"
)
func TestDockerEngineOnContainerdMultiTest(t *testing.T) {
defer func() {
err := e2eengine.CleanupEngine(t)
if err != nil {
t.Errorf("Failed to cleanup engine: %s", err)
}
}()
t.Log("Attempt engine init without experimental")
// First init
result := icmd.RunCmd(icmd.Command("docker", "engine", "init"),
func(c *icmd.Cmd) {
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=disabled")
})
result.Assert(t, icmd.Expected{
Out: "",
Err: "docker engine init is only supported",
ExitCode: 1,
})
t.Log("First engine init")
// First init
result = icmd.RunCmd(icmd.Command("docker", "engine", "init"),
func(c *icmd.Cmd) {
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
})
result.Assert(t, icmd.Expected{
Out: "Success! The docker engine is now running.",
Err: "",
ExitCode: 0,
})
t.Log("checking for updates")
// Check for updates
result = icmd.RunCmd(icmd.Command("docker", "engine", "check", "--downgrades", "--pre-releases"))
result.Assert(t, icmd.Expected{
Out: "VERSION",
Err: "",
ExitCode: 0,
})
t.Log("attempt second init (should fail)")
// Attempt to init a second time and fail
result = icmd.RunCmd(icmd.Command("docker", "engine", "init"),
func(c *icmd.Cmd) {
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
})
result.Assert(t, icmd.Expected{
Out: "",
Err: "engine already present",
ExitCode: 1,
})
t.Log("perform update")
// Now update and succeed
targetVersion := os.Getenv("VERSION")
result = icmd.RunCmd(icmd.Command("docker", "engine", "update", "--version", targetVersion))
result.Assert(t, icmd.Expected{
Out: "Success! The docker engine is now running.",
Err: "",
ExitCode: 0,
})
t.Log("remove engine")
result = icmd.RunCmd(icmd.Command("docker", "engine", "rm"),
func(c *icmd.Cmd) {
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
})
result.Assert(t, icmd.Expected{
Out: "",
Err: "",
ExitCode: 0,
})
}
-39
View File
@@ -1,39 +0,0 @@
package e2eengine
import (
"context"
"strings"
"testing"
"github.com/docker/cli/internal/containerizedengine"
)
// CleanupEngine ensures the local engine has been removed between testcases
func CleanupEngine(t *testing.T) error {
t.Log("doing engine cleanup")
ctx := context.Background()
client, err := containerizedengine.NewClient("")
if err != nil {
return err
}
// See if the engine exists first
engine, err := client.GetEngine(ctx)
if err != nil {
if strings.Contains(err.Error(), "not present") {
t.Log("engine was not detected, no cleanup to perform")
// Nothing to do, it's not defined
return nil
}
t.Logf("failed to lookup engine: %s", err)
// Any other error is not good...
return err
}
// TODO Consider nuking the docker dir too so there's no cached content between test cases
err = client.RemoveEngine(ctx, engine)
if err != nil {
t.Logf("Failed to remove engine: %s", err)
}
return err
}
+14 -144
View File
@@ -1,12 +1,9 @@
package containerizedengine package containerizedengine
import ( import (
"bytes"
"context" "context"
"syscall"
"github.com/containerd/containerd" "github.com/containerd/containerd"
containerdtypes "github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/cio" "github.com/containerd/containerd/cio"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content" "github.com/containerd/containerd/content"
@@ -14,7 +11,6 @@ import (
prototypes "github.com/gogo/protobuf/types" prototypes "github.com/gogo/protobuf/types"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go"
) )
type ( type (
@@ -25,6 +21,8 @@ type (
getImageFunc func(ctx context.Context, ref string) (containerd.Image, error) getImageFunc func(ctx context.Context, ref string) (containerd.Image, error)
contentStoreFunc func() content.Store contentStoreFunc func() content.Store
containerServiceFunc func() containers.Store containerServiceFunc func() containers.Store
installFunc func(context.Context, containerd.Image, ...containerd.InstallOpts) error
versionFunc func(ctx context.Context) (containerd.Version, error)
} }
fakeContainer struct { fakeContainer struct {
idFunc func() string idFunc func() string
@@ -49,30 +47,6 @@ type (
isUnpackedFunc func(context.Context, string) (bool, error) isUnpackedFunc func(context.Context, string) (bool, error)
contentStoreFunc func() content.Store contentStoreFunc func() content.Store
} }
fakeTask struct {
idFunc func() string
pidFunc func() uint32
startFunc func(context.Context) error
deleteFunc func(context.Context, ...containerd.ProcessDeleteOpts) (*containerd.ExitStatus, error)
killFunc func(context.Context, syscall.Signal, ...containerd.KillOpts) error
waitFunc func(context.Context) (<-chan containerd.ExitStatus, error)
closeIOFunc func(context.Context, ...containerd.IOCloserOpts) error
resizeFunc func(ctx context.Context, w, h uint32) error
ioFunc func() cio.IO
statusFunc func(context.Context) (containerd.Status, error)
pauseFunc func(context.Context) error
resumeFunc func(context.Context) error
execFunc func(context.Context, string, *specs.Process, cio.Creator) (containerd.Process, error)
pidsFunc func(context.Context) ([]containerd.ProcessInfo, error)
checkpointFunc func(context.Context, ...containerd.CheckpointTaskOpts) (containerd.Image, error)
updateFunc func(context.Context, ...containerd.UpdateTaskOpts) error
loadProcessFunc func(context.Context, string, cio.Attach) (containerd.Process, error)
metricsFunc func(context.Context) (*containerdtypes.Metric, error)
}
testOutStream struct {
bytes.Buffer
}
) )
func (w *fakeContainerdClient) Containers(ctx context.Context, filters ...string) ([]containerd.Container, error) { func (w *fakeContainerdClient) Containers(ctx context.Context, filters ...string) ([]containerd.Container, error) {
@@ -114,6 +88,18 @@ func (w *fakeContainerdClient) ContainerService() containers.Store {
func (w *fakeContainerdClient) Close() error { func (w *fakeContainerdClient) Close() error {
return nil return nil
} }
func (w *fakeContainerdClient) Install(ctx context.Context, image containerd.Image, args ...containerd.InstallOpts) error {
if w.installFunc != nil {
return w.installFunc(ctx, image, args...)
}
return nil
}
func (w *fakeContainerdClient) Version(ctx context.Context) (containerd.Version, error) {
if w.versionFunc != nil {
return w.versionFunc(ctx)
}
return containerd.Version{}, nil
}
func (c *fakeContainer) ID() string { func (c *fakeContainer) ID() string {
if c.idFunc != nil { if c.idFunc != nil {
@@ -230,119 +216,3 @@ func (i *fakeImage) ContentStore() content.Store {
} }
return nil return nil
} }
func (t *fakeTask) ID() string {
if t.idFunc != nil {
return t.idFunc()
}
return ""
}
func (t *fakeTask) Pid() uint32 {
if t.pidFunc != nil {
return t.pidFunc()
}
return 0
}
func (t *fakeTask) Start(ctx context.Context) error {
if t.startFunc != nil {
return t.startFunc(ctx)
}
return nil
}
func (t *fakeTask) Delete(ctx context.Context, opts ...containerd.ProcessDeleteOpts) (*containerd.ExitStatus, error) {
if t.deleteFunc != nil {
return t.deleteFunc(ctx, opts...)
}
return nil, nil
}
func (t *fakeTask) Kill(ctx context.Context, signal syscall.Signal, opts ...containerd.KillOpts) error {
if t.killFunc != nil {
return t.killFunc(ctx, signal, opts...)
}
return nil
}
func (t *fakeTask) Wait(ctx context.Context) (<-chan containerd.ExitStatus, error) {
if t.waitFunc != nil {
return t.waitFunc(ctx)
}
return nil, nil
}
func (t *fakeTask) CloseIO(ctx context.Context, opts ...containerd.IOCloserOpts) error {
if t.closeIOFunc != nil {
return t.closeIOFunc(ctx, opts...)
}
return nil
}
func (t *fakeTask) Resize(ctx context.Context, w, h uint32) error {
if t.resizeFunc != nil {
return t.resizeFunc(ctx, w, h)
}
return nil
}
func (t *fakeTask) IO() cio.IO {
if t.ioFunc != nil {
return t.ioFunc()
}
return nil
}
func (t *fakeTask) Status(ctx context.Context) (containerd.Status, error) {
if t.statusFunc != nil {
return t.statusFunc(ctx)
}
return containerd.Status{}, nil
}
func (t *fakeTask) Pause(ctx context.Context) error {
if t.pauseFunc != nil {
return t.pauseFunc(ctx)
}
return nil
}
func (t *fakeTask) Resume(ctx context.Context) error {
if t.resumeFunc != nil {
return t.resumeFunc(ctx)
}
return nil
}
func (t *fakeTask) Exec(ctx context.Context, cmd string, proc *specs.Process, ioc cio.Creator) (containerd.Process, error) {
if t.execFunc != nil {
return t.execFunc(ctx, cmd, proc, ioc)
}
return nil, nil
}
func (t *fakeTask) Pids(ctx context.Context) ([]containerd.ProcessInfo, error) {
if t.pidsFunc != nil {
return t.pidsFunc(ctx)
}
return nil, nil
}
func (t *fakeTask) Checkpoint(ctx context.Context, opts ...containerd.CheckpointTaskOpts) (containerd.Image, error) {
if t.checkpointFunc != nil {
return t.checkpointFunc(ctx, opts...)
}
return nil, nil
}
func (t *fakeTask) Update(ctx context.Context, opts ...containerd.UpdateTaskOpts) error {
if t.updateFunc != nil {
return t.updateFunc(ctx, opts...)
}
return nil
}
func (t *fakeTask) LoadProcess(ctx context.Context, name string, attach cio.Attach) (containerd.Process, error) {
if t.loadProcessFunc != nil {
return t.loadProcessFunc(ctx, name, attach)
}
return nil, nil
}
func (t *fakeTask) Metrics(ctx context.Context) (*containerdtypes.Metric, error) {
if t.metricsFunc != nil {
return t.metricsFunc(ctx)
}
return nil, nil
}
func (o *testOutStream) FD() uintptr {
return 0
}
func (o *testOutStream) IsTerminal() bool {
return false
}
+5 -4
View File
@@ -7,6 +7,7 @@ import (
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/images" "github.com/containerd/containerd/images"
"github.com/containerd/containerd/remotes/docker" "github.com/containerd/containerd/remotes/docker"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/jsonmessage"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -15,7 +16,7 @@ import (
// NewClient returns a new containerizedengine client // NewClient returns a new containerizedengine client
// This client can be used to manage the lifecycle of // This client can be used to manage the lifecycle of
// dockerd running as a container on containerd. // dockerd running as a container on containerd.
func NewClient(sockPath string) (Client, error) { func NewClient(sockPath string) (clitypes.ContainerizedClient, error) {
if sockPath == "" { if sockPath == "" {
sockPath = containerdSockPath sockPath = containerdSockPath
} }
@@ -23,17 +24,17 @@ func NewClient(sockPath string) (Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return baseClient{ return &baseClient{
cclient: cclient, cclient: cclient,
}, nil }, nil
} }
// Close will close the underlying clients // Close will close the underlying clients
func (c baseClient) Close() error { func (c *baseClient) Close() error {
return c.cclient.Close() return c.cclient.Close()
} }
func (c baseClient) pullWithAuth(ctx context.Context, imageName string, out OutStream, func (c *baseClient) pullWithAuth(ctx context.Context, imageName string, out clitypes.OutStream,
authConfig *types.AuthConfig) (containerd.Image, error) { authConfig *types.AuthConfig) (containerd.Image, error) {
resolver := docker.NewResolver(docker.ResolverOptions{ resolver := docker.NewResolver(docker.ResolverOptions{
@@ -1,11 +1,13 @@
package containerizedengine package containerizedengine
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"testing" "testing"
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"gotest.tools/assert" "gotest.tools/assert"
) )
@@ -22,7 +24,7 @@ func TestPullWithAuthPullFail(t *testing.T) {
} }
imageName := "testnamegoeshere" imageName := "testnamegoeshere"
_, err := client.pullWithAuth(ctx, imageName, &testOutStream{}, &types.AuthConfig{}) _, err := client.pullWithAuth(ctx, imageName, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, "pull failure") assert.ErrorContains(t, err, "pull failure")
} }
@@ -38,6 +40,6 @@ func TestPullWithAuthPullPass(t *testing.T) {
} }
imageName := "testnamegoeshere" imageName := "testnamegoeshere"
_, err := client.pullWithAuth(ctx, imageName, &testOutStream{}, &types.AuthConfig{}) _, err := client.pullWithAuth(ctx, imageName, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.NilError(t, err) assert.NilError(t, err)
} }
-261
View File
@@ -1,261 +0,0 @@
package containerizedengine
import (
"context"
"fmt"
"io"
"strings"
"syscall"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/runtime/restart"
"github.com/docker/cli/internal/pkg/containerized"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
)
// InitEngine is the main entrypoint for `docker engine init`
func (c baseClient) InitEngine(ctx context.Context, opts EngineInitOptions, out OutStream,
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
ctx = namespaces.WithNamespace(ctx, engineNamespace)
// Verify engine isn't already running
_, err := c.GetEngine(ctx)
if err == nil {
return ErrEngineAlreadyPresent
} else if err != ErrEngineNotPresent {
return err
}
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
// Look for desired image
_, err = c.cclient.GetImage(ctx, imageName)
if err != nil {
if errdefs.IsNotFound(err) {
_, err = c.pullWithAuth(ctx, imageName, out, authConfig)
if err != nil {
return errors.Wrapf(err, "unable to pull image %s", imageName)
}
} else {
return errors.Wrapf(err, "unable to check for image %s", imageName)
}
}
// Spin up the engine
err = c.startEngineOnContainerd(ctx, imageName, opts.ConfigFile)
if err != nil {
return errors.Wrap(err, "failed to create docker daemon")
}
// Wait for the daemon to start, verify it's responsive
fmt.Fprintf(out, "Waiting for engine to start... ")
ctx, cancel := context.WithTimeout(ctx, engineWaitTimeout)
defer cancel()
if err := c.waitForEngine(ctx, out, healthfn); err != nil {
// TODO once we have the logging strategy sorted out
// this should likely gather the last few lines of logs to report
// why the daemon failed to initialize
return errors.Wrap(err, "failed to start docker daemon")
}
fmt.Fprintf(out, "Success! The docker engine is now running.\n")
return nil
}
// GetEngine will return the containerd container running the engine (or error)
func (c baseClient) GetEngine(ctx context.Context) (containerd.Container, error) {
ctx = namespaces.WithNamespace(ctx, engineNamespace)
containers, err := c.cclient.Containers(ctx, "id=="+engineContainerName)
if err != nil {
return nil, err
}
if len(containers) == 0 {
return nil, ErrEngineNotPresent
}
return containers[0], nil
}
// getEngineImage will return the current image used by the engine
func (c baseClient) getEngineImage(engine containerd.Container) (string, error) {
ctx := namespaces.WithNamespace(context.Background(), engineNamespace)
image, err := engine.Image(ctx)
if err != nil {
return "", err
}
return image.Name(), nil
}
// getEngineConfigFilePath will extract the config file location from the engine flags
func (c baseClient) getEngineConfigFilePath(ctx context.Context, engine containerd.Container) (string, error) {
spec, err := engine.Spec(ctx)
configFile := ""
if err != nil {
return configFile, err
}
for i := 0; i < len(spec.Process.Args); i++ {
arg := spec.Process.Args[i]
if strings.HasPrefix(arg, "--config-file") {
if strings.Contains(arg, "=") {
split := strings.SplitN(arg, "=", 2)
configFile = split[1]
} else {
if i+1 >= len(spec.Process.Args) {
return configFile, ErrMalformedConfigFileParam
}
configFile = spec.Process.Args[i+1]
}
}
}
if configFile == "" {
// TODO - any more diagnostics to offer?
return configFile, ErrEngineConfigLookupFailure
}
return configFile, nil
}
var (
engineWaitInterval = 500 * time.Millisecond
engineWaitTimeout = 60 * time.Second
)
// waitForEngine will wait for the engine to start
func (c baseClient) waitForEngine(ctx context.Context, out io.Writer, healthfn func(context.Context) error) error {
ticker := time.NewTicker(engineWaitInterval)
defer ticker.Stop()
defer func() {
fmt.Fprintf(out, "\n")
}()
err := c.waitForEngineContainer(ctx, ticker)
if err != nil {
return err
}
fmt.Fprintf(out, "waiting for engine to be responsive... ")
for {
select {
case <-ticker.C:
err = healthfn(ctx)
if err == nil {
fmt.Fprintf(out, "engine is online.")
return nil
}
case <-ctx.Done():
return errors.Wrap(err, "timeout waiting for engine to be responsive")
}
}
}
func (c baseClient) waitForEngineContainer(ctx context.Context, ticker *time.Ticker) error {
var ret error
for {
select {
case <-ticker.C:
engine, err := c.GetEngine(ctx)
if engine != nil {
return nil
}
ret = err
case <-ctx.Done():
return errors.Wrap(ret, "timeout waiting for engine to be responsive")
}
}
}
// RemoveEngine gracefully unwinds the current engine
func (c baseClient) RemoveEngine(ctx context.Context, engine containerd.Container) error {
ctx = namespaces.WithNamespace(ctx, engineNamespace)
// Make sure the container isn't being restarted while we unwind it
stopLabel := map[string]string{}
stopLabel[restart.StatusLabel] = string(containerd.Stopped)
engine.SetLabels(ctx, stopLabel)
// Wind down the existing engine
task, err := engine.Task(ctx, nil)
if err != nil {
if !errdefs.IsNotFound(err) {
return err
}
} else {
status, err := task.Status(ctx)
if err != nil {
return err
}
if status.Status == containerd.Running {
// It's running, so kill it
err := task.Kill(ctx, syscall.SIGTERM, []containerd.KillOpts{}...)
if err != nil {
return errors.Wrap(err, "task kill error")
}
ch, err := task.Wait(ctx)
if err != nil {
return err
}
timeout := time.NewTimer(engineWaitTimeout)
select {
case <-timeout.C:
// TODO - consider a force flag in the future to allow a more aggressive
// kill of the engine via
// task.Kill(ctx, syscall.SIGKILL, containerd.WithKillAll)
return ErrEngineShutdownTimeout
case <-ch:
}
}
if _, err := task.Delete(ctx); err != nil {
return err
}
}
deleteOpts := []containerd.DeleteOpts{containerd.WithSnapshotCleanup}
err = engine.Delete(ctx, deleteOpts...)
if err != nil && errdefs.IsNotFound(err) {
return nil
}
return errors.Wrap(err, "failed to remove existing engine container")
}
// startEngineOnContainerd creates a new docker engine running on containerd
func (c baseClient) startEngineOnContainerd(ctx context.Context, imageName, configFile string) error {
ctx = namespaces.WithNamespace(ctx, engineNamespace)
image, err := c.cclient.GetImage(ctx, imageName)
if err != nil {
if errdefs.IsNotFound(err) {
return fmt.Errorf("engine image missing: %s", imageName)
}
return errors.Wrap(err, "failed to check for engine image")
}
// Make sure we have a valid config file
err = c.verifyDockerConfig(configFile)
if err != nil {
return err
}
engineSpec.Process.Args = append(engineSpec.Process.Args,
"--config-file", configFile,
)
cOpts := []containerd.NewContainerOpts{
containerized.WithNewSnapshot(image),
restart.WithStatus(containerd.Running),
restart.WithLogPath("/var/log/engine.log"), // TODO - better!
genSpec(),
containerd.WithRuntime("io.containerd.runtime.process.v1", nil),
}
_, err = c.cclient.NewContainer(
ctx,
engineContainerName,
cOpts...,
)
if err != nil {
return errors.Wrap(err, "failed to create engine container")
}
return nil
}
-537
View File
@@ -1,537 +0,0 @@
package containerizedengine
import (
"context"
"fmt"
"syscall"
"testing"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/oci"
"github.com/docker/docker/api/types"
"github.com/opencontainers/runtime-spec/specs-go"
"gotest.tools/assert"
)
func healthfnHappy(ctx context.Context) error {
return nil
}
func healthfnError(ctx context.Context) error {
return fmt.Errorf("ping failure")
}
func TestInitGetEngineFail(t *testing.T) {
ctx := context.Background()
opts := EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: CommunityEngineImage,
}
container := &fakeContainer{}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
assert.Assert(t, err == ErrEngineAlreadyPresent)
}
func TestInitCheckImageFail(t *testing.T) {
ctx := context.Background()
opts := EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: CommunityEngineImage,
}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return nil, fmt.Errorf("something went wrong")
},
},
}
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
assert.ErrorContains(t, err, "unable to check for image")
assert.ErrorContains(t, err, "something went wrong")
}
func TestInitPullFail(t *testing.T) {
ctx := context.Background()
opts := EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: CommunityEngineImage,
}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return nil, errdefs.ErrNotFound
},
pullFunc: func(ctx context.Context, ref string, opts ...containerd.RemoteOpt) (containerd.Image, error) {
return nil, fmt.Errorf("pull failure")
},
},
}
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
assert.ErrorContains(t, err, "unable to pull image")
assert.ErrorContains(t, err, "pull failure")
}
func TestInitStartFail(t *testing.T) {
ctx := context.Background()
opts := EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: CommunityEngineImage,
}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return nil, errdefs.ErrNotFound
},
pullFunc: func(ctx context.Context, ref string, opts ...containerd.RemoteOpt) (containerd.Image, error) {
return nil, nil
},
},
}
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
assert.ErrorContains(t, err, "failed to create docker daemon")
}
func TestGetEngineFail(t *testing.T) {
ctx := context.Background()
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return nil, fmt.Errorf("container failure")
},
},
}
_, err := client.GetEngine(ctx)
assert.ErrorContains(t, err, "failure")
}
func TestGetEngineNotPresent(t *testing.T) {
ctx := context.Background()
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
},
}
_, err := client.GetEngine(ctx)
assert.Assert(t, err == ErrEngineNotPresent)
}
func TestGetEngineFound(t *testing.T) {
ctx := context.Background()
container := &fakeContainer{}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
c, err := client.GetEngine(ctx)
assert.NilError(t, err)
assert.Equal(t, c, container)
}
func TestGetEngineImageFail(t *testing.T) {
client := baseClient{}
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return nil, fmt.Errorf("failure")
},
}
_, err := client.getEngineImage(container)
assert.ErrorContains(t, err, "failure")
}
func TestGetEngineImagePass(t *testing.T) {
client := baseClient{}
image := &fakeImage{
nameFunc: func() string {
return "imagenamehere"
},
}
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return image, nil
},
}
name, err := client.getEngineImage(container)
assert.NilError(t, err)
assert.Equal(t, name, "imagenamehere")
}
func TestWaitForEngineNeverShowsUp(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
engineWaitInterval = 1 * time.Millisecond
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
},
}
err := client.waitForEngine(ctx, &testOutStream{}, healthfnError)
assert.ErrorContains(t, err, "timeout waiting")
}
func TestWaitForEnginePingFail(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
engineWaitInterval = 1 * time.Millisecond
container := &fakeContainer{}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
err := client.waitForEngine(ctx, &testOutStream{}, healthfnError)
assert.ErrorContains(t, err, "ping fail")
}
func TestWaitForEngineHealthy(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
engineWaitInterval = 1 * time.Millisecond
container := &fakeContainer{}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
err := client.waitForEngine(ctx, &testOutStream{}, healthfnHappy)
assert.NilError(t, err)
}
func TestRemoveEngineBadTaskBadDelete(t *testing.T) {
ctx := context.Background()
client := baseClient{}
container := &fakeContainer{
deleteFunc: func(context.Context, ...containerd.DeleteOpts) error {
return fmt.Errorf("delete failure")
},
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return nil, errdefs.ErrNotFound
},
}
err := client.RemoveEngine(ctx, container)
assert.ErrorContains(t, err, "failed to remove existing engine")
assert.ErrorContains(t, err, "delete failure")
}
func TestRemoveEngineTaskNoStatus(t *testing.T) {
ctx := context.Background()
client := baseClient{}
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{}, fmt.Errorf("task status failure")
},
}
container := &fakeContainer{
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return task, nil
},
}
err := client.RemoveEngine(ctx, container)
assert.ErrorContains(t, err, "task status failure")
}
func TestRemoveEngineTaskNotRunningDeleteFail(t *testing.T) {
ctx := context.Background()
client := baseClient{}
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{Status: containerd.Unknown}, nil
},
deleteFunc: func(context.Context, ...containerd.ProcessDeleteOpts) (*containerd.ExitStatus, error) {
return nil, fmt.Errorf("task delete failure")
},
}
container := &fakeContainer{
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return task, nil
},
}
err := client.RemoveEngine(ctx, container)
assert.ErrorContains(t, err, "task delete failure")
}
func TestRemoveEngineTaskRunningKillFail(t *testing.T) {
ctx := context.Background()
client := baseClient{}
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{Status: containerd.Running}, nil
},
killFunc: func(context.Context, syscall.Signal, ...containerd.KillOpts) error {
return fmt.Errorf("task kill failure")
},
}
container := &fakeContainer{
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return task, nil
},
}
err := client.RemoveEngine(ctx, container)
assert.ErrorContains(t, err, "task kill failure")
}
func TestRemoveEngineTaskRunningWaitFail(t *testing.T) {
ctx := context.Background()
client := baseClient{}
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{Status: containerd.Running}, nil
},
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
return nil, fmt.Errorf("task wait failure")
},
}
container := &fakeContainer{
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return task, nil
},
}
err := client.RemoveEngine(ctx, container)
assert.ErrorContains(t, err, "task wait failure")
}
func TestRemoveEngineTaskRunningHappyPath(t *testing.T) {
ctx := context.Background()
client := baseClient{}
ch := make(chan containerd.ExitStatus, 1)
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{Status: containerd.Running}, nil
},
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
ch <- containerd.ExitStatus{}
return ch, nil
},
}
container := &fakeContainer{
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return task, nil
},
}
err := client.RemoveEngine(ctx, container)
assert.NilError(t, err)
}
func TestRemoveEngineTaskKillTimeout(t *testing.T) {
ctx := context.Background()
ch := make(chan containerd.ExitStatus, 1)
client := baseClient{}
engineWaitTimeout = 10 * time.Millisecond
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{Status: containerd.Running}, nil
},
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
//ch <- containerd.ExitStatus{} // let it timeout
return ch, nil
},
}
container := &fakeContainer{
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
return task, nil
},
}
err := client.RemoveEngine(ctx, container)
assert.Assert(t, err == ErrEngineShutdownTimeout)
}
func TestStartEngineOnContainerdImageErr(t *testing.T) {
ctx := context.Background()
imageName := "testnamegoeshere"
configFile := "/tmp/configfilegoeshere"
client := baseClient{
cclient: &fakeContainerdClient{
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return nil, fmt.Errorf("some image lookup failure")
},
},
}
err := client.startEngineOnContainerd(ctx, imageName, configFile)
assert.ErrorContains(t, err, "some image lookup failure")
}
func TestStartEngineOnContainerdImageNotFound(t *testing.T) {
ctx := context.Background()
imageName := "testnamegoeshere"
configFile := "/tmp/configfilegoeshere"
client := baseClient{
cclient: &fakeContainerdClient{
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return nil, errdefs.ErrNotFound
},
},
}
err := client.startEngineOnContainerd(ctx, imageName, configFile)
assert.ErrorContains(t, err, "engine image missing")
}
func TestStartEngineOnContainerdHappy(t *testing.T) {
ctx := context.Background()
imageName := "testnamegoeshere"
configFile := "/tmp/configfilegoeshere"
ch := make(chan containerd.ExitStatus, 1)
streams := cio.Streams{}
task := &fakeTask{
statusFunc: func(context.Context) (containerd.Status, error) {
return containerd.Status{Status: containerd.Running}, nil
},
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
ch <- containerd.ExitStatus{}
return ch, nil
},
}
container := &fakeContainer{
newTaskFunc: func(ctx context.Context, creator cio.Creator, opts ...containerd.NewTaskOpts) (containerd.Task, error) {
if streams.Stdout != nil {
streams.Stdout.Write([]byte("{}"))
}
return task, nil
},
}
client := baseClient{
cclient: &fakeContainerdClient{
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return nil, nil
},
newContainerFunc: func(ctx context.Context, id string, opts ...containerd.NewContainerOpts) (containerd.Container, error) {
return container, nil
},
},
}
err := client.startEngineOnContainerd(ctx, imageName, configFile)
assert.NilError(t, err)
}
func TestGetEngineConfigFilePathBadSpec(t *testing.T) {
ctx := context.Background()
client := baseClient{}
container := &fakeContainer{
specFunc: func(context.Context) (*oci.Spec, error) {
return nil, fmt.Errorf("spec error")
},
}
_, err := client.getEngineConfigFilePath(ctx, container)
assert.ErrorContains(t, err, "spec error")
}
func TestGetEngineConfigFilePathDistinct(t *testing.T) {
ctx := context.Background()
client := baseClient{}
container := &fakeContainer{
specFunc: func(context.Context) (*oci.Spec, error) {
return &oci.Spec{
Process: &specs.Process{
Args: []string{
"--another-flag",
"foo",
"--config-file",
"configpath",
},
},
}, nil
},
}
configFile, err := client.getEngineConfigFilePath(ctx, container)
assert.NilError(t, err)
assert.Assert(t, err, configFile == "configpath")
}
func TestGetEngineConfigFilePathEquals(t *testing.T) {
ctx := context.Background()
client := baseClient{}
container := &fakeContainer{
specFunc: func(context.Context) (*oci.Spec, error) {
return &oci.Spec{
Process: &specs.Process{
Args: []string{
"--another-flag=foo",
"--config-file=configpath",
},
},
}, nil
},
}
configFile, err := client.getEngineConfigFilePath(ctx, container)
assert.NilError(t, err)
assert.Assert(t, err, configFile == "configpath")
}
func TestGetEngineConfigFilePathMalformed1(t *testing.T) {
ctx := context.Background()
client := baseClient{}
container := &fakeContainer{
specFunc: func(context.Context) (*oci.Spec, error) {
return &oci.Spec{
Process: &specs.Process{
Args: []string{
"--another-flag",
"--config-file",
},
},
}, nil
},
}
_, err := client.getEngineConfigFilePath(ctx, container)
assert.Assert(t, err == ErrMalformedConfigFileParam)
}
@@ -1,16 +0,0 @@
// +build !windows
package containerizedengine
import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/oci"
"github.com/docker/cli/internal/pkg/containerized"
)
func genSpec() containerd.NewContainerOpts {
return containerd.WithSpec(&engineSpec,
containerized.WithAllCapabilities,
oci.WithParentCgroupDevices,
)
}
@@ -1,14 +0,0 @@
// +build windows
package containerizedengine
import (
"github.com/containerd/containerd"
"github.com/docker/cli/internal/pkg/containerized"
)
func genSpec() containerd.NewContainerOpts {
return containerd.WithSpec(&engineSpec,
containerized.WithAllCapabilities,
)
}
-35
View File
@@ -1,35 +0,0 @@
package containerizedengine
import (
"os"
"path"
)
func (c baseClient) verifyDockerConfig(configFile string) error {
// TODO - in the future consider leveraging containerd and a host runtime
// to create the file. For now, just create it locally since we have to be
// local to talk to containerd
configDir := path.Dir(configFile)
err := os.MkdirAll(configDir, 0644)
if err != nil {
return err
}
fd, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
return err
}
defer fd.Close()
info, err := fd.Stat()
if err != nil {
return err
}
if info.Size() == 0 {
_, err := fd.Write([]byte("{}"))
return err
}
return nil
}
+3 -3
View File
@@ -64,7 +64,7 @@ outer:
} }
} }
err := updateNonActive(ctx, ongoing, cs, statuses, keys, activeSeen, &done, start) err := updateNonActive(ctx, ongoing, cs, statuses, &keys, activeSeen, &done, start)
if err != nil { if err != nil {
continue outer continue outer
} }
@@ -92,11 +92,11 @@ outer:
} }
} }
func updateNonActive(ctx context.Context, ongoing *jobs, cs content.Store, statuses map[string]statusInfo, keys []string, activeSeen map[string]struct{}, done *bool, start time.Time) error { func updateNonActive(ctx context.Context, ongoing *jobs, cs content.Store, statuses map[string]statusInfo, keys *[]string, activeSeen map[string]struct{}, done *bool, start time.Time) error {
for _, j := range ongoing.jobs() { for _, j := range ongoing.jobs() {
key := remotes.MakeRefKey(ctx, j) key := remotes.MakeRefKey(ctx, j)
keys = append(keys, key) *keys = append(*keys, key)
if _, ok := activeSeen[key]; ok { if _, ok := activeSeen[key]; ok {
continue continue
} }
@@ -1,12 +0,0 @@
// +build !windows
package containerizedengine
import (
"golang.org/x/sys/unix"
)
var (
// SIGKILL maps to unix.SIGKILL
SIGKILL = unix.SIGKILL
)
@@ -1,12 +0,0 @@
// +build windows
package containerizedengine
import (
"syscall"
)
var (
// SIGKILL all signals are ignored by containerd kill windows
SIGKILL = syscall.Signal(0)
)
+4 -114
View File
@@ -3,30 +3,15 @@ package containerizedengine
import ( import (
"context" "context"
"errors" "errors"
"io"
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
"github.com/containerd/containerd/content" "github.com/containerd/containerd/content"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/docker/api/types"
ver "github.com/hashicorp/go-version"
specs "github.com/opencontainers/runtime-spec/specs-go"
) )
const ( const (
// CommunityEngineImage is the repo name for the community engine containerdSockPath = "/run/containerd/containerd.sock"
CommunityEngineImage = "engine-community" engineNamespace = "com.docker"
// EnterpriseEngineImage is the repo name for the enterprise engine
EnterpriseEngineImage = "engine-enterprise"
containerdSockPath = "/run/containerd/containerd.sock"
engineContainerName = "dockerd"
engineNamespace = "docker"
// Used to signal the containerd-proxy if it should manage
proxyLabel = "com.docker/containerd-proxy.scope"
) )
var ( var (
@@ -44,80 +29,12 @@ var (
// ErrEngineShutdownTimeout returned if the engine failed to shutdown in time // ErrEngineShutdownTimeout returned if the engine failed to shutdown in time
ErrEngineShutdownTimeout = errors.New("timeout waiting for engine to exit") ErrEngineShutdownTimeout = errors.New("timeout waiting for engine to exit")
// ErrEngineImageMissingTag returned if the engine image is missing the version tag
ErrEngineImageMissingTag = errors.New("malformed engine image missing tag")
engineSpec = specs.Spec{
Root: &specs.Root{
Path: "rootfs",
},
Process: &specs.Process{
Cwd: "/",
Args: []string{
// In general, configuration should be driven by the config file, not these flags
// TODO - consider moving more of these to the config file, and make sure the defaults are set if not present.
"/sbin/dockerd",
"-s",
"overlay2",
"--containerd",
"/run/containerd/containerd.sock",
"--default-runtime",
"containerd",
"--add-runtime",
"containerd=runc",
},
User: specs.User{
UID: 0,
GID: 0,
},
Env: []string{
"PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
},
NoNewPrivileges: false,
},
}
) )
// Client can be used to manage the lifecycle of
// dockerd running as a container on containerd.
type Client interface {
Close() error
ActivateEngine(ctx context.Context,
opts EngineInitOptions,
out OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
InitEngine(ctx context.Context,
opts EngineInitOptions,
out OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
DoUpdate(ctx context.Context,
opts EngineInitOptions,
out OutStream,
authConfig *types.AuthConfig,
healthfn func(context.Context) error) error
GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (AvailableVersions, error)
GetEngine(ctx context.Context) (containerd.Container, error)
RemoveEngine(ctx context.Context, engine containerd.Container) error
GetCurrentEngineVersion(ctx context.Context) (EngineInitOptions, error)
}
type baseClient struct { type baseClient struct {
cclient containerdClient cclient containerdClient
} }
// EngineInitOptions contains the configuration settings
// use during initialization of a containerized docker engine
type EngineInitOptions struct {
RegistryPrefix string
EngineImage string
EngineVersion string
ConfigFile string
scope string
}
// containerdClient abstracts the containerd client to aid in testability // containerdClient abstracts the containerd client to aid in testability
type containerdClient interface { type containerdClient interface {
Containers(ctx context.Context, filters ...string) ([]containerd.Container, error) Containers(ctx context.Context, filters ...string) ([]containerd.Container, error)
@@ -127,33 +44,6 @@ type containerdClient interface {
Close() error Close() error
ContentStore() content.Store ContentStore() content.Store
ContainerService() containers.Store ContainerService() containers.Store
} Install(context.Context, containerd.Image, ...containerd.InstallOpts) error
Version(ctx context.Context) (containerd.Version, error)
// AvailableVersions groups the available versions which were discovered
type AvailableVersions struct {
Downgrades []DockerVersion
Patches []DockerVersion
Upgrades []DockerVersion
}
// DockerVersion wraps a semantic version to retain the original tag
// since the docker date based versions don't strictly follow semantic
// versioning (leading zeros, etc.)
type DockerVersion struct {
ver.Version
Tag string
}
// Update stores available updates for rendering in a table
type Update struct {
Type string
Version string
Notes string
}
// OutStream is an output stream used to write normal program output.
type OutStream interface {
io.Writer
FD() uintptr
IsTerminal() bool
} }
+125 -72
View File
@@ -2,79 +2,54 @@ package containerizedengine
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"path"
"strings" "strings"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/namespaces"
"github.com/docker/cli/internal/pkg/containerized" "github.com/docker/cli/internal/versions"
clitypes "github.com/docker/cli/types"
"github.com/docker/distribution/reference" "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
ver "github.com/hashicorp/go-version"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// GetCurrentEngineVersion determines the current type of engine (image) and version
func (c baseClient) GetCurrentEngineVersion(ctx context.Context) (EngineInitOptions, error) {
ctx = namespaces.WithNamespace(ctx, engineNamespace)
ret := EngineInitOptions{}
currentEngine := CommunityEngineImage
engine, err := c.GetEngine(ctx)
if err != nil {
if err == ErrEngineNotPresent {
return ret, errors.Wrap(err, "failed to find existing engine")
}
return ret, err
}
imageName, err := c.getEngineImage(engine)
if err != nil {
return ret, err
}
distributionRef, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return ret, errors.Wrapf(err, "failed to parse image name: %s", imageName)
}
if strings.Contains(distributionRef.Name(), EnterpriseEngineImage) {
currentEngine = EnterpriseEngineImage
}
taggedRef, ok := distributionRef.(reference.NamedTagged)
if !ok {
return ret, ErrEngineImageMissingTag
}
ret.EngineImage = currentEngine
ret.EngineVersion = taggedRef.Tag()
ret.RegistryPrefix = reference.Domain(taggedRef) + "/" + path.Dir(reference.Path(taggedRef))
return ret, nil
}
// ActivateEngine will switch the image from the CE to EE image // ActivateEngine will switch the image from the CE to EE image
func (c baseClient) ActivateEngine(ctx context.Context, opts EngineInitOptions, out OutStream, func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
authConfig *types.AuthConfig, healthfn func(context.Context) error) error { authConfig *types.AuthConfig) error {
// set the proxy scope to "ee" for activate flows // If the user didn't specify an image, determine the correct enterprise image to use
opts.scope = "ee" if opts.EngineImage == "" {
localMetadata, err := versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
ctx = namespaces.WithNamespace(ctx, engineNamespace)
// If version is unspecified, use the existing engine version
if opts.EngineVersion == "" {
currentOpts, err := c.GetCurrentEngineVersion(ctx)
if err != nil { if err != nil {
return err return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image")
} }
opts.EngineVersion = currentOpts.EngineVersion
if currentOpts.EngineImage == EnterpriseEngineImage { engineImage := localMetadata.EngineImage
// This is a "no-op" activation so the only change would be the license - don't update the engine itself if engineImage == clitypes.EnterpriseEngineImage || engineImage == clitypes.CommunityEngineImage {
return nil opts.EngineImage = clitypes.EnterpriseEngineImage
} else {
// Chop off the standard prefix and retain any trailing OS specific image details
// e.g., engine-community-dm -> engine-enterprise-dm
engineImage = strings.TrimPrefix(engineImage, clitypes.EnterpriseEngineImage)
engineImage = strings.TrimPrefix(engineImage, clitypes.CommunityEngineImage)
opts.EngineImage = clitypes.EnterpriseEngineImage + engineImage
} }
} }
return c.DoUpdate(ctx, opts, out, authConfig, healthfn)
ctx = namespaces.WithNamespace(ctx, engineNamespace)
return c.DoUpdate(ctx, opts, out, authConfig)
} }
// DoUpdate performs the underlying engine update // DoUpdate performs the underlying engine update
func (c baseClient) DoUpdate(ctx context.Context, opts EngineInitOptions, out OutStream, func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
authConfig *types.AuthConfig, healthfn func(context.Context) error) error { authConfig *types.AuthConfig) error {
ctx = namespaces.WithNamespace(ctx, engineNamespace) ctx = namespaces.WithNamespace(ctx, engineNamespace)
if opts.EngineVersion == "" { if opts.EngineVersion == "" {
@@ -83,7 +58,16 @@ func (c baseClient) DoUpdate(ctx context.Context, opts EngineInitOptions, out Ou
// current engine version and automatically apply it so users // current engine version and automatically apply it so users
// could stay in sync by simply having a scheduled // could stay in sync by simply having a scheduled
// `docker engine update` // `docker engine update`
return fmt.Errorf("please pick the version you want to update to") return fmt.Errorf("pick the version you want to update to with --version")
}
var localMetadata *clitypes.RuntimeMetadata
if opts.EngineImage == "" {
var err error
localMetadata, err = versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
if err != nil {
return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image set to 'engine-community' or 'engine-enterprise'")
}
opts.EngineImage = localMetadata.EngineImage
} }
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion) imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
@@ -101,30 +85,99 @@ func (c baseClient) DoUpdate(ctx context.Context, opts EngineInitOptions, out Ou
} }
} }
// Gather information about the existing engine so we can recreate it // Make sure we're safe to proceed
engine, err := c.GetEngine(ctx) newMetadata, err := c.PreflightCheck(ctx, image)
if err != nil { if err != nil {
if err == ErrEngineNotPresent { return err
return errors.Wrap(err, "unable to find existing engine - please use init") }
if localMetadata != nil {
if localMetadata.Platform != newMetadata.Platform {
fmt.Fprintf(out, "\nNotice: you have switched to \"%s\". Refer to %s for update instructions.\n\n", newMetadata.Platform, getReleaseNotesURL(imageName))
} }
}
if err := c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr")); err != nil {
return err return err
} }
// TODO verify the image has changed and don't update if nothing has changed return versions.WriteRuntimeMetadata(opts.RuntimeMetadataDir, newMetadata)
}
err = containerized.AtomicImageUpdate(ctx, engine, image, func() error { // PreflightCheck verifies the specified image is compatible with the local system before proceeding to update/activate
ctx, cancel := context.WithTimeout(ctx, engineWaitTimeout) // If things look good, the RuntimeMetadata for the new image is returned and can be written out to the host
defer cancel() func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) (*clitypes.RuntimeMetadata, error) {
return c.waitForEngine(ctx, out, healthfn) var metadata clitypes.RuntimeMetadata
}) ic, err := image.Config(ctx)
if err == nil && opts.scope != "" { if err != nil {
var labels map[string]string return nil, err
labels, err = engine.Labels(ctx) }
var (
ociimage v1.Image
config v1.ImageConfig
)
switch ic.MediaType {
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
if err != nil { if err != nil {
return err return nil, err
}
if err := json.Unmarshal(p, &ociimage); err != nil {
return nil, err
}
config = ociimage.Config
default:
return nil, fmt.Errorf("unknown image %s config media type %s", image.Name(), ic.MediaType)
}
metadataString, ok := config.Labels["com.docker."+clitypes.RuntimeMetadataName]
if !ok {
return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), clitypes.RuntimeMetadataName)
}
err = json.Unmarshal([]byte(metadataString), &metadata)
if err != nil {
return nil, errors.Wrapf(err, "malformed runtime metadata file in %s", image.Name())
}
// Current CLI only supports host install runtime
if metadata.Runtime != "host_install" {
return nil, fmt.Errorf("unsupported daemon image: %s\nConsult the release notes at %s for upgrade instructions", metadata.Runtime, getReleaseNotesURL(image.Name()))
}
// Verify local containerd is new enough
localVersion, err := c.cclient.Version(ctx)
if err != nil {
return nil, err
}
if metadata.ContainerdMinVersion != "" {
lv, err := ver.NewVersion(localVersion.Version)
if err != nil {
return nil, err
}
mv, err := ver.NewVersion(metadata.ContainerdMinVersion)
if err != nil {
return nil, err
}
if lv.LessThan(mv) {
return nil, fmt.Errorf("local containerd is too old: %s - this engine version requires %s or newer.\nConsult the release notes at %s for upgrade instructions",
localVersion.Version, metadata.ContainerdMinVersion, getReleaseNotesURL(image.Name()))
}
} // If omitted on metadata, no hard dependency on containerd version beyond 18.09 baseline
// All checks look OK, proceed with update
return &metadata, nil
}
// getReleaseNotesURL returns a release notes url
// If the image name does not contain a version tag, the base release notes URL is returned
func getReleaseNotesURL(imageName string) string {
versionTag := ""
distributionRef, err := reference.ParseNormalizedNamed(imageName)
if err == nil {
taggedRef, ok := distributionRef.(reference.NamedTagged)
if ok {
versionTag = taggedRef.Tag()
} }
labels[proxyLabel] = opts.scope
_, err = engine.SetLabels(ctx, labels)
} }
return err return fmt.Sprintf("%s?%s", clitypes.ReleaseNotePrefix, versionTag)
} }
+167 -185
View File
@@ -1,169 +1,78 @@
package containerizedengine package containerizedengine
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io/ioutil"
"os"
"testing" "testing"
"github.com/containerd/containerd" "github.com/containerd/containerd"
"github.com/containerd/containerd/cio" "github.com/containerd/containerd/cio"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/versions"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/assert" "gotest.tools/assert"
) )
func TestGetCurrentEngineVersionHappy(t *testing.T) { func TestActivateImagePermutations(t *testing.T) {
ctx := context.Background() ctx := context.Background()
image := &fakeImage{ lookedup := "not called yet"
nameFunc: func() string { expectedError := fmt.Errorf("expected error")
return "acme.com/dockermirror/" + CommunityEngineImage + ":engineversion"
},
}
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return image, nil
},
}
client := baseClient{ client := baseClient{
cclient: &fakeContainerdClient{ cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return []containerd.Container{container}, nil lookedup = ref
},
},
}
opts, err := client.GetCurrentEngineVersion(ctx)
assert.NilError(t, err)
assert.Equal(t, opts.EngineImage, CommunityEngineImage)
assert.Equal(t, opts.RegistryPrefix, "acme.com/dockermirror")
assert.Equal(t, opts.EngineVersion, "engineversion")
}
func TestGetCurrentEngineVersionEnterpriseHappy(t *testing.T) {
ctx := context.Background()
image := &fakeImage{
nameFunc: func() string {
return "docker.io/docker/" + EnterpriseEngineImage + ":engineversion"
},
}
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return image, nil
},
}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
opts, err := client.GetCurrentEngineVersion(ctx)
assert.NilError(t, err)
assert.Equal(t, opts.EngineImage, EnterpriseEngineImage)
assert.Equal(t, opts.EngineVersion, "engineversion")
assert.Equal(t, opts.RegistryPrefix, "docker.io/docker")
}
func TestGetCurrentEngineVersionNoEngine(t *testing.T) {
ctx := context.Background()
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
},
}
_, err := client.GetCurrentEngineVersion(ctx)
assert.ErrorContains(t, err, "failed to find existing engine")
}
func TestGetCurrentEngineVersionMiscEngineError(t *testing.T) {
ctx := context.Background()
expectedError := fmt.Errorf("some container lookup error")
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return nil, expectedError return nil, expectedError
}, },
}, },
} }
tmpdir, err := ioutil.TempDir("", "enginedir")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
_, err := client.GetCurrentEngineVersion(ctx) opts := clitypes.EngineInitOptions{
assert.Assert(t, err == expectedError) EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
RuntimeMetadataDir: tmpdir,
}
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, expectedError.Error())
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion))
metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, expectedError.Error())
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion))
metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage + "-dm"}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, expectedError.Error())
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage+"-dm", opts.EngineVersion))
} }
func TestGetCurrentEngineVersionImageFailure(t *testing.T) { func TestActivateConfigFailure(t *testing.T) {
ctx := context.Background()
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return nil, fmt.Errorf("container image failure")
},
}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
_, err := client.GetCurrentEngineVersion(ctx)
assert.ErrorContains(t, err, "container image failure")
}
func TestGetCurrentEngineVersionMalformed(t *testing.T) {
ctx := context.Background()
image := &fakeImage{
nameFunc: func() string {
return "imagename"
},
}
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return image, nil
},
}
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
},
}
_, err := client.GetCurrentEngineVersion(ctx)
assert.Assert(t, err == ErrEngineImageMissingTag)
}
func TestActivateNoEngine(t *testing.T) {
ctx := context.Background()
client := baseClient{
cclient: &fakeContainerdClient{
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
},
}
opts := EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: EnterpriseEngineImage,
}
err := client.ActivateEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
assert.ErrorContains(t, err, "unable to find")
}
func TestActivateNoChange(t *testing.T) {
ctx := context.Background() ctx := context.Background()
registryPrefix := "registryprefixgoeshere" registryPrefix := "registryprefixgoeshere"
image := &fakeImage{ image := &fakeImage{
nameFunc: func() string { nameFunc: func() string {
return registryPrefix + "/" + EnterpriseEngineImage + ":engineversion" return registryPrefix + "/" + clitypes.EnterpriseEngineImage + ":engineversion"
},
configFunc: func(ctx context.Context) (ocispec.Descriptor, error) {
return ocispec.Descriptor{}, fmt.Errorf("config lookup failure")
}, },
} }
container := &fakeContainer{ container := &fakeContainer{
@@ -182,17 +91,27 @@ func TestActivateNoChange(t *testing.T) {
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil return []containerd.Container{container}, nil
}, },
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return image, nil
},
}, },
} }
opts := EngineInitOptions{ tmpdir, err := ioutil.TempDir("", "engindir")
EngineVersion: "engineversiongoeshere", assert.NilError(t, err)
RegistryPrefix: "registryprefixgoeshere", defer os.RemoveAll(tmpdir)
ConfigFile: "/tmp/configfilegoeshere", metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
EngineImage: EnterpriseEngineImage, err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
opts := clitypes.EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: clitypes.EnterpriseEngineImage,
RuntimeMetadataDir: tmpdir,
} }
err := client.ActivateEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy) err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.NilError(t, err) assert.ErrorContains(t, err, "config lookup failure")
} }
func TestActivateDoUpdateFail(t *testing.T) { func TestActivateDoUpdateFail(t *testing.T) {
@@ -219,38 +138,60 @@ func TestActivateDoUpdateFail(t *testing.T) {
}, },
}, },
} }
opts := EngineInitOptions{ tmpdir, err := ioutil.TempDir("", "enginedir")
EngineVersion: "engineversiongoeshere", assert.NilError(t, err)
RegistryPrefix: "registryprefixgoeshere", defer os.RemoveAll(tmpdir)
ConfigFile: "/tmp/configfilegoeshere", metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
EngineImage: EnterpriseEngineImage, err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
opts := clitypes.EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: clitypes.EnterpriseEngineImage,
RuntimeMetadataDir: tmpdir,
} }
err := client.ActivateEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy) err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, "check for image") assert.ErrorContains(t, err, "check for image")
assert.ErrorContains(t, err, "something went wrong") assert.ErrorContains(t, err, "something went wrong")
} }
func TestDoUpdateNoVersion(t *testing.T) { func TestDoUpdateNoVersion(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "enginedir")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
ctx := context.Background() ctx := context.Background()
opts := EngineInitOptions{ opts := clitypes.EngineInitOptions{
EngineVersion: "", EngineVersion: "",
RegistryPrefix: "registryprefixgoeshere", RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere", ConfigFile: "/tmp/configfilegoeshere",
EngineImage: EnterpriseEngineImage, EngineImage: clitypes.EnterpriseEngineImage,
RuntimeMetadataDir: tmpdir,
} }
client := baseClient{} client := baseClient{}
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy) err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, "please pick the version you") assert.ErrorContains(t, err, "pick the version you")
} }
func TestDoUpdateImageMiscError(t *testing.T) { func TestDoUpdateImageMiscError(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := EngineInitOptions{ tmpdir, err := ioutil.TempDir("", "enginedir")
EngineVersion: "engineversiongoeshere", assert.NilError(t, err)
RegistryPrefix: "registryprefixgoeshere", defer os.RemoveAll(tmpdir)
ConfigFile: "/tmp/configfilegoeshere", metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
EngineImage: "testnamegoeshere", err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
opts := clitypes.EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: "testnamegoeshere",
RuntimeMetadataDir: tmpdir,
} }
client := baseClient{ client := baseClient{
cclient: &fakeContainerdClient{ cclient: &fakeContainerdClient{
@@ -260,18 +201,26 @@ func TestDoUpdateImageMiscError(t *testing.T) {
}, },
}, },
} }
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, "check for image") assert.ErrorContains(t, err, "check for image")
assert.ErrorContains(t, err, "something went wrong") assert.ErrorContains(t, err, "something went wrong")
} }
func TestDoUpdatePullFail(t *testing.T) { func TestDoUpdatePullFail(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := EngineInitOptions{ tmpdir, err := ioutil.TempDir("", "enginedir")
EngineVersion: "engineversiongoeshere", assert.NilError(t, err)
RegistryPrefix: "registryprefixgoeshere", defer os.RemoveAll(tmpdir)
ConfigFile: "/tmp/configfilegoeshere", metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
EngineImage: "testnamegoeshere", err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
opts := clitypes.EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: "testnamegoeshere",
RuntimeMetadataDir: tmpdir,
} }
client := baseClient{ client := baseClient{
cclient: &fakeContainerdClient{ cclient: &fakeContainerdClient{
@@ -284,35 +233,68 @@ func TestDoUpdatePullFail(t *testing.T) {
}, },
}, },
} }
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, "unable to pull") assert.ErrorContains(t, err, "unable to pull")
assert.ErrorContains(t, err, "pull failure") assert.ErrorContains(t, err, "pull failure")
} }
func TestDoUpdateEngineMissing(t *testing.T) { func TestActivateDoUpdateVerifyImageName(t *testing.T) {
ctx := context.Background() ctx := context.Background()
opts := EngineInitOptions{ registryPrefix := "registryprefixgoeshere"
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: "testnamegoeshere",
}
image := &fakeImage{ image := &fakeImage{
nameFunc: func() string { nameFunc: func() string {
return "imagenamehere" return registryPrefix + "/ce-engine:engineversion"
}, },
} }
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
return image, nil
},
}
requestedImage := "unset"
client := baseClient{ client := baseClient{
cclient: &fakeContainerdClient{ cclient: &fakeContainerdClient{
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return image, nil
},
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) { containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil return []containerd.Container{container}, nil
},
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
requestedImage = ref
return nil, fmt.Errorf("something went wrong")
}, },
}, },
} }
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy) tmpdir, err := ioutil.TempDir("", "enginedir")
assert.ErrorContains(t, err, "unable to find existing engine") assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
opts := clitypes.EngineInitOptions{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
EngineImage: "testnamegoeshere",
ConfigFile: "/tmp/configfilegoeshere",
RuntimeMetadataDir: tmpdir,
}
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
assert.ErrorContains(t, err, "check for image")
assert.ErrorContains(t, err, "something went wrong")
expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage)
}
func TestGetReleaseNotesURL(t *testing.T) {
imageName := "bogus image name #$%&@!"
url := getReleaseNotesURL(imageName)
assert.Equal(t, url, clitypes.ReleaseNotePrefix+"?")
imageName = "foo.bar/valid/repowithouttag"
url = getReleaseNotesURL(imageName)
assert.Equal(t, url, clitypes.ReleaseNotePrefix+"?")
imageName = "foo.bar/valid/repowithouttag:tag123"
url = getReleaseNotesURL(imageName)
assert.Equal(t, url, clitypes.ReleaseNotePrefix+"?tag123")
} }
-72
View File
@@ -1,72 +0,0 @@
package containerizedengine
import (
"context"
"sort"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/distribution/reference"
ver "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// GetEngineVersions reports the versions of the engine that are available
func (c baseClient) GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (AvailableVersions, error) {
imageRef, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return AvailableVersions{}, err
}
tags, err := registryClient.GetTags(ctx, imageRef)
if err != nil {
return AvailableVersions{}, err
}
return parseTags(tags, currentVersion)
}
func parseTags(tags []string, currentVersion string) (AvailableVersions, error) {
var ret AvailableVersions
currentVer, err := ver.NewVersion(currentVersion)
if err != nil {
return ret, errors.Wrapf(err, "failed to parse existing version %s", currentVersion)
}
downgrades := []DockerVersion{}
patches := []DockerVersion{}
upgrades := []DockerVersion{}
currentSegments := currentVer.Segments()
for _, tag := range tags {
tmp, err := ver.NewVersion(tag)
if err != nil {
logrus.Debugf("Unable to parse %s: %s", tag, err)
continue
}
testVersion := DockerVersion{Version: *tmp, Tag: tag}
if testVersion.LessThan(currentVer) {
downgrades = append(downgrades, testVersion)
continue
}
testSegments := testVersion.Segments()
// lib always provides min 3 segments
if testSegments[0] == currentSegments[0] &&
testSegments[1] == currentSegments[1] {
patches = append(patches, testVersion)
} else {
upgrades = append(upgrades, testVersion)
}
}
sort.Slice(downgrades, func(i, j int) bool {
return downgrades[i].Version.LessThan(&downgrades[j].Version)
})
sort.Slice(patches, func(i, j int) bool {
return patches[i].Version.LessThan(&patches[j].Version)
})
sort.Slice(upgrades, func(i, j int) bool {
return upgrades[i].Version.LessThan(&upgrades[j].Version)
})
ret.Downgrades = downgrades
ret.Patches = patches
ret.Upgrades = upgrades
return ret, nil
}
+11 -3
View File
@@ -13,13 +13,14 @@ type (
getHubUserOrgsFunc func(ctx context.Context, authToken string) (orgs []model.Org, err error) getHubUserOrgsFunc func(ctx context.Context, authToken string) (orgs []model.Org, err error)
getHubUserByNameFunc func(ctx context.Context, username string) (user *model.User, err error) getHubUserByNameFunc func(ctx context.Context, username string) (user *model.User, err error)
verifyLicenseFunc func(ctx context.Context, license model.IssuedLicense) (res *model.CheckResponse, err error) verifyLicenseFunc func(ctx context.Context, license model.IssuedLicense) (res *model.CheckResponse, err error)
generateNewTrialSubscriptionFunc func(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) generateNewTrialSubscriptionFunc func(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error)
listSubscriptionsFunc func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) listSubscriptionsFunc func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error)
listSubscriptionsDetailsFunc func(ctx context.Context, authToken, dockerID string) (response []*model.SubscriptionDetail, err error) listSubscriptionsDetailsFunc func(ctx context.Context, authToken, dockerID string) (response []*model.SubscriptionDetail, err error)
downloadLicenseFromHubFunc func(ctx context.Context, authToken, subscriptionID string) (license *model.IssuedLicense, err error) downloadLicenseFromHubFunc func(ctx context.Context, authToken, subscriptionID string) (license *model.IssuedLicense, err error)
parseLicenseFunc func(license []byte) (parsedLicense *model.IssuedLicense, err error) parseLicenseFunc func(license []byte) (parsedLicense *model.IssuedLicense, err error)
storeLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error storeLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error
loadLocalLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error) loadLocalLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error)
summarizeLicenseFunc func(*model.CheckResponse, string) *model.Subscription
} }
) )
@@ -51,9 +52,9 @@ func (c *fakeLicensingClient) VerifyLicense(ctx context.Context, license model.I
return nil, nil return nil, nil
} }
func (c *fakeLicensingClient) GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) { func (c *fakeLicensingClient) GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error) {
if c.generateNewTrialSubscriptionFunc != nil { if c.generateNewTrialSubscriptionFunc != nil {
return c.generateNewTrialSubscriptionFunc(ctx, authToken, dockerID, email) return c.generateNewTrialSubscriptionFunc(ctx, authToken, dockerID)
} }
return "", nil return "", nil
} }
@@ -102,3 +103,10 @@ func (c *fakeLicensingClient) LoadLocalLicense(ctx context.Context, dclnt licens
} }
return nil, nil return nil, nil
} }
func (c *fakeLicensingClient) SummarizeLicense(cr *model.CheckResponse, keyid string) *model.Subscription {
if c.summarizeLicenseFunc != nil {
return c.summarizeLicenseFunc(cr, keyid)
}
return nil
}
+4 -1
View File
@@ -10,7 +10,10 @@ var (
// licensingPublicKey is the official public license key for store.docker.com // licensingPublicKey is the official public license key for store.docker.com
// nolint: lll // nolint: lll
licensingPublicKey = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0Ka2lkOiBKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVAoKTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF5ZEl5K2xVN283UGNlWSs0K3MrQwpRNU9FZ0N5RjhDeEljUUlXdUs4NHBJaVpjaVk2NzMweUNZbndMU0tUbHcrVTZVQy9RUmVXUmlvTU5ORTVEczVUCllFWGJHRzZvbG0ycWRXYkJ3Y0NnKzJVVUgvT2NCOVd1UDZnUlBIcE1GTXN4RHpXd3ZheThKVXVIZ1lVTFVwbTEKSXYrbXE3bHA1blEvUnhyVDBLWlJBUVRZTEVNRWZHd20zaE1PL2dlTFBTK2hnS1B0SUhsa2c2L1djb3hUR29LUAo3OWQvd2FIWXhHTmw3V2hTbmVpQlN4YnBiUUFLazIxbGc3OThYYjd2WnlFQVRETXJSUjlNZUU2QWRqNUhKcFkzCkNveVJBUENtYUtHUkNLNHVvWlNvSXUwaEZWbEtVUHliYncwMDBHTyt3YTJLTjhVd2dJSW0waTVJMXVXOUdrcTQKempCeTV6aGdxdVVYYkc5YldQQU9ZcnE1UWE4MUR4R2NCbEp5SFlBcCtERFBFOVRHZzR6WW1YakpueFpxSEVkdQpHcWRldlo4WE1JMHVrZmtHSUkxNHdVT2lNSUlJclhsRWNCZi80Nkk4Z1FXRHp4eWNaZS9KR1grTEF1YXlYcnlyClVGZWhWTlVkWlVsOXdYTmFKQitrYUNxejVRd2FSOTNzR3crUVNmdEQwTnZMZTdDeU9IK0U2dmc2U3QvTmVUdmcKdjhZbmhDaVhJbFo4SE9mSXdOZTd0RUYvVWN6NU9iUHlrbTN0eWxyTlVqdDBWeUFtdHRhY1ZJMmlHaWhjVVBybQprNGxWSVo3VkQvTFNXK2k3eW9TdXJ0cHNQWGNlMnBLRElvMzBsSkdoTy8zS1VtbDJTVVpDcXpKMXlFbUtweXNICjVIRFc5Y3NJRkNBM2RlQWpmWlV2TjdVQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=" licensingPublicKeys = []string{
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0Ka2lkOiBKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVAoKTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF5ZEl5K2xVN283UGNlWSs0K3MrQwpRNU9FZ0N5RjhDeEljUUlXdUs4NHBJaVpjaVk2NzMweUNZbndMU0tUbHcrVTZVQy9RUmVXUmlvTU5ORTVEczVUCllFWGJHRzZvbG0ycWRXYkJ3Y0NnKzJVVUgvT2NCOVd1UDZnUlBIcE1GTXN4RHpXd3ZheThKVXVIZ1lVTFVwbTEKSXYrbXE3bHA1blEvUnhyVDBLWlJBUVRZTEVNRWZHd20zaE1PL2dlTFBTK2hnS1B0SUhsa2c2L1djb3hUR29LUAo3OWQvd2FIWXhHTmw3V2hTbmVpQlN4YnBiUUFLazIxbGc3OThYYjd2WnlFQVRETXJSUjlNZUU2QWRqNUhKcFkzCkNveVJBUENtYUtHUkNLNHVvWlNvSXUwaEZWbEtVUHliYncwMDBHTyt3YTJLTjhVd2dJSW0waTVJMXVXOUdrcTQKempCeTV6aGdxdVVYYkc5YldQQU9ZcnE1UWE4MUR4R2NCbEp5SFlBcCtERFBFOVRHZzR6WW1YakpueFpxSEVkdQpHcWRldlo4WE1JMHVrZmtHSUkxNHdVT2lNSUlJclhsRWNCZi80Nkk4Z1FXRHp4eWNaZS9KR1grTEF1YXlYcnlyClVGZWhWTlVkWlVsOXdYTmFKQitrYUNxejVRd2FSOTNzR3crUVNmdEQwTnZMZTdDeU9IK0U2dmc2U3QvTmVUdmcKdjhZbmhDaVhJbFo4SE9mSXdOZTd0RUYvVWN6NU9iUHlrbTN0eWxyTlVqdDBWeUFtdHRhY1ZJMmlHaWhjVVBybQprNGxWSVo3VkQvTFNXK2k3eW9TdXJ0cHNQWGNlMnBLRElvMzBsSkdoTy8zS1VtbDJTVVpDcXpKMXlFbUtweXNICjVIRFc5Y3NJRkNBM2RlQWpmWlV2TjdVQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=",
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0Ka2V5SUQ6IFpGSjI6Q1c1Szo1M0tSOlo0NUg6NlpVQzpJNFhFOlpUS1A6TVQ1UjpQWFpMOlNTNE46RjQ0NDo0U1Q0CmtpZDogWkZKMjpDVzVLOjUzS1I6WjQ1SDo2WlVDOkk0WEU6WlRLUDpNVDVSOlBYWkw6U1M0TjpGNDQ0OjRTVDQKCk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBd1FhVVRaUFhQZnloZFZVdkJkbXkKZlViYXZYL1pmdkNkMCtGREdNb0ZQazlUTlE1aVZPSkhaUVVNa2N2d2QrdVdaV3dvdWtEUGhZaWxEQTZ6Y3krQQowdERFQkF0Nmc5TGM3UFNXU1BZMTJpbWxnbC85RmJzQnZsSjFRc1RJNGlPUjQ1K0FsMHMxMWhaNG0wR1k4UXQ4CnpFN0RYU1BNUzVRTHlUcHlEemZkQURVcWFGRVcxNTVOQ3BaKzZ6N0lHZCt0V2xjalB3QzQwb3ppbWM1bXVUSWgKb2w1WG1hUFREYk45VzhDWGQ1ZWdUeEExZU43YTA3MWR0R1RialFMUEhvb0QxRURsbitvZjZ2VGFReUphWWJmQgpNRHF2NFdraG9QSzJPWWZ5OXVLR1lTNS9ieHIzUWVTUGRoWVFrQzl2YVZsRUtuTjFZaER6VXZVZGR1c3lyRUdICjd3SURBUUFCCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo=",
}
) )
type ( type (
+36 -23
View File
@@ -1,6 +1,7 @@
package licenseutils package licenseutils
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -19,7 +20,7 @@ import (
// HubUser wraps a licensing client and holds key information // HubUser wraps a licensing client and holds key information
// for a user to avoid multiple lookups // for a user to avoid multiple lookups
type HubUser struct { type HubUser struct {
client licensing.Client Client licensing.Client
token string token string
User model.User User model.User
Orgs []model.Org Orgs []model.Org
@@ -35,18 +36,22 @@ func (u HubUser) GetOrgByID(orgID string) (model.Org, error) {
return model.Org{}, fmt.Errorf("org %s not found", orgID) return model.Org{}, fmt.Errorf("org %s not found", orgID)
} }
// Login to the license server and return a client that can be used to look up and download license files or generate new trial licenses func getClient() (licensing.Client, error) {
func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
baseURI, err := url.Parse(licensingDefaultBaseURI) baseURI, err := url.Parse(licensingDefaultBaseURI)
if err != nil { if err != nil {
return HubUser{}, err return nil, err
} }
lclient, err := licensing.New(&licensing.Config{ return licensing.New(&licensing.Config{
BaseURI: *baseURI, BaseURI: *baseURI,
HTTPClient: &http.Client{}, HTTPClient: &http.Client{},
PublicKey: licensingPublicKey, PublicKeys: licensingPublicKeys,
}) })
}
// Login to the license server and return a client that can be used to look up and download license files or generate new trial licenses
func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
lclient, err := getClient()
if err != nil { if err != nil {
return HubUser{}, err return HubUser{}, err
} }
@@ -68,7 +73,7 @@ func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
return HubUser{}, err return HubUser{}, err
} }
return HubUser{ return HubUser{
client: lclient, Client: lclient,
token: token, token: token,
User: *user, User: *user,
Orgs: orgs, Orgs: orgs,
@@ -78,12 +83,12 @@ func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
// GetAvailableLicenses finds all available licenses for a given account and their orgs // GetAvailableLicenses finds all available licenses for a given account and their orgs
func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, error) { func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, error) {
subs, err := u.client.ListSubscriptions(ctx, u.token, u.User.ID) subs, err := u.Client.ListSubscriptions(ctx, u.token, u.User.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, org := range u.Orgs { for _, org := range u.Orgs {
orgSub, err := u.client.ListSubscriptions(ctx, u.token, org.ID) orgSub, err := u.Client.ListSubscriptions(ctx, u.token, org.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -97,7 +102,7 @@ func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, er
// Filter out expired licenses // Filter out expired licenses
i := 0 i := 0
for _, s := range subs { for _, s := range subs {
if s.State != "expired" && s.Expires != nil { if s.State == "active" && s.Expires != nil {
owner := "" owner := ""
if s.DockerID == u.User.ID { if s.DockerID == u.User.ID {
owner = u.User.Username owner = u.User.Username
@@ -129,42 +134,50 @@ func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, er
// GenerateTrialLicense will generate a new trial license for the specified user or org // GenerateTrialLicense will generate a new trial license for the specified user or org
func (u HubUser) GenerateTrialLicense(ctx context.Context, targetID string) (*model.IssuedLicense, error) { func (u HubUser) GenerateTrialLicense(ctx context.Context, targetID string) (*model.IssuedLicense, error) {
subID, err := u.client.GenerateNewTrialSubscription(ctx, u.token, targetID, u.User.Email) subID, err := u.Client.GenerateNewTrialSubscription(ctx, u.token, targetID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return u.client.DownloadLicenseFromHub(ctx, u.token, subID) return u.Client.DownloadLicenseFromHub(ctx, u.token, subID)
} }
// GetIssuedLicense will download a license by ID // GetIssuedLicense will download a license by ID
func (u HubUser) GetIssuedLicense(ctx context.Context, ID string) (*model.IssuedLicense, error) { func (u HubUser) GetIssuedLicense(ctx context.Context, ID string) (*model.IssuedLicense, error) {
return u.client.DownloadLicenseFromHub(ctx, u.token, ID) return u.Client.DownloadLicenseFromHub(ctx, u.token, ID)
} }
// LoadLocalIssuedLicense will load a local license file // LoadLocalIssuedLicense will load a local license file
func LoadLocalIssuedLicense(ctx context.Context, filename string) (*model.IssuedLicense, error) { func LoadLocalIssuedLicense(ctx context.Context, filename string) (*model.IssuedLicense, error) {
baseURI, err := url.Parse(licensingDefaultBaseURI) lclient, err := getClient()
if err != nil {
return nil, err
}
lclient, err := licensing.New(&licensing.Config{
BaseURI: *baseURI,
HTTPClient: &http.Client{},
PublicKey: licensingPublicKey,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
return doLoadLocalIssuedLicense(ctx, filename, lclient) return doLoadLocalIssuedLicense(ctx, filename, lclient)
} }
// GetLicenseSummary summarizes the license for the user
func GetLicenseSummary(ctx context.Context, license model.IssuedLicense) (string, error) {
lclient, err := getClient()
if err != nil {
return "", err
}
cr, err := lclient.VerifyLicense(ctx, license)
if err != nil {
return "", err
}
return lclient.SummarizeLicense(cr, license.KeyID).String(), nil
}
func doLoadLocalIssuedLicense(ctx context.Context, filename string, lclient licensing.Client) (*model.IssuedLicense, error) { func doLoadLocalIssuedLicense(ctx context.Context, filename string, lclient licensing.Client) (*model.IssuedLicense, error) {
var license model.IssuedLicense var license model.IssuedLicense
data, err := ioutil.ReadFile(filename) data, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// The file may contain a leading BOM, which will choke the
// json deserializer.
data = bytes.TrimPrefix(data, []byte("\xef\xbb\xbf"))
err = json.Unmarshal(data, &license) err = json.Unmarshal(data, &license)
if err != nil { if err != nil {
+8 -8
View File
@@ -43,7 +43,7 @@ func TestGetOrgByID(t *testing.T) {
func TestGetAvailableLicensesListFail(t *testing.T) { func TestGetAvailableLicensesListFail(t *testing.T) {
ctx := context.Background() ctx := context.Background()
user := HubUser{ user := HubUser{
client: &fakeLicensingClient{ Client: &fakeLicensingClient{
listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) { listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
return nil, fmt.Errorf("list subscriptions error") return nil, fmt.Errorf("list subscriptions error")
}, },
@@ -59,7 +59,7 @@ func TestGetAvailableLicensesOrgFail(t *testing.T) {
Orgs: []model.Org{ Orgs: []model.Org{
{ID: "orgid"}, {ID: "orgid"},
}, },
client: &fakeLicensingClient{ Client: &fakeLicensingClient{
listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) { listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
if dockerID == "orgid" { if dockerID == "orgid" {
return nil, fmt.Errorf("list subscriptions org error") return nil, fmt.Errorf("list subscriptions org error")
@@ -86,7 +86,7 @@ func TestGetAvailableLicensesHappy(t *testing.T) {
Orgname: "orgname", Orgname: "orgname",
}, },
}, },
client: &fakeLicensingClient{ Client: &fakeLicensingClient{
listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) { listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
if dockerID == "orgid" { if dockerID == "orgid" {
return []*model.Subscription{ return []*model.Subscription{
@@ -146,8 +146,8 @@ func TestGetAvailableLicensesHappy(t *testing.T) {
func TestGenerateTrialFail(t *testing.T) { func TestGenerateTrialFail(t *testing.T) {
ctx := context.Background() ctx := context.Background()
user := HubUser{ user := HubUser{
client: &fakeLicensingClient{ Client: &fakeLicensingClient{
generateNewTrialSubscriptionFunc: func(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) { generateNewTrialSubscriptionFunc: func(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error) {
return "", fmt.Errorf("generate trial failure") return "", fmt.Errorf("generate trial failure")
}, },
}, },
@@ -160,8 +160,8 @@ func TestGenerateTrialFail(t *testing.T) {
func TestGenerateTrialHappy(t *testing.T) { func TestGenerateTrialHappy(t *testing.T) {
ctx := context.Background() ctx := context.Background()
user := HubUser{ user := HubUser{
client: &fakeLicensingClient{ Client: &fakeLicensingClient{
generateNewTrialSubscriptionFunc: func(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) { generateNewTrialSubscriptionFunc: func(ctx context.Context, authToken, dockerID string) (subscriptionID string, err error) {
return "subid", nil return "subid", nil
}, },
}, },
@@ -174,7 +174,7 @@ func TestGenerateTrialHappy(t *testing.T) {
func TestGetIssuedLicense(t *testing.T) { func TestGetIssuedLicense(t *testing.T) {
ctx := context.Background() ctx := context.Background()
user := HubUser{ user := HubUser{
client: &fakeLicensingClient{}, Client: &fakeLicensingClient{},
} }
id := "idgoeshere" id := "idgoeshere"
_, err := user.GetIssuedLicense(ctx, id) _, err := user.GetIssuedLicense(ctx, id)
+66
View File
@@ -0,0 +1,66 @@
package containerized
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
)
var (
proxydir = "/etc/containerd-proxy"
)
type proxyConfig struct {
ID string `json:"-"`
Namespace string `json:"namespace"`
Image string `json:"image"`
ImagePath string `json:"imagePath"`
Args []string `json:"args"`
Scope string `json:"scope"`
}
func updateConfig(name, newImage string) error {
cfg, err := loadConfig(name)
if err != nil && os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
cfg.Image = newImage
cfg.ImagePath = ""
return storeConfig(name, cfg)
}
func loadConfig(name string) (*proxyConfig, error) {
configFile := filepath.Join(proxydir, name+".json")
data, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, err
}
var cfg proxyConfig
err = json.Unmarshal(data, &cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}
// storeConfig will write out the config only if it already exists
func storeConfig(name string, cfg *proxyConfig) error {
configFile := filepath.Join(proxydir, name+".json")
fd, err := os.OpenFile(configFile, os.O_RDWR, 0644)
if err != nil && os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
err = fd.Truncate(0)
if err != nil {
return err
}
enc := json.NewEncoder(fd)
return enc.Encode(cfg)
}
+68
View File
@@ -0,0 +1,68 @@
package containerized
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"testing"
"gotest.tools/assert"
)
func TestUpdateConfigNotExist(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "cfg-update")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
origProxyDir := proxydir
defer func() {
proxydir = origProxyDir
}()
proxydir = tmpdir
name := "myname"
newImage := "newimage:foo"
err = updateConfig(name, newImage)
assert.NilError(t, err)
}
func TestUpdateConfigBadJson(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "cfg-update")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
origProxyDir := proxydir
defer func() {
proxydir = origProxyDir
}()
proxydir = tmpdir
filename := filepath.Join(tmpdir, "dockerd.json")
err = ioutil.WriteFile(filename, []byte("not json"), 0644)
assert.NilError(t, err)
name := "dockerd"
newImage := "newimage:foo"
err = updateConfig(name, newImage)
assert.ErrorContains(t, err, "invalid character")
}
func TestUpdateConfigHappyPath(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "cfg-update")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
origProxyDir := proxydir
defer func() {
proxydir = origProxyDir
}()
proxydir = tmpdir
filename := filepath.Join(tmpdir, "dockerd.json")
err = ioutil.WriteFile(filename, []byte("{}"), 0644)
assert.NilError(t, err)
name := "dockerd"
newImage := "newimage:foo"
err = updateConfig(name, newImage)
assert.NilError(t, err)
data, err := ioutil.ReadFile(filename)
assert.NilError(t, err)
var cfg map[string]string
err = json.Unmarshal(data, &cfg)
assert.NilError(t, err)
assert.Assert(t, cfg["image"] == newImage)
}
+8
View File
@@ -50,6 +50,10 @@ func WithUpgrade(i containerd.Image) containerd.UpdateContainerOpts {
return err return err
} }
c.Image = i.Name() c.Image = i.Name()
err = updateConfig(c.ID, c.Image)
if err != nil {
return err
}
c.SnapshotKey = revision.Key c.SnapshotKey = revision.Key
return nil return nil
} }
@@ -74,6 +78,10 @@ func WithRollback(ctx context.Context, client *containerd.Client, c *containers.
return fmt.Errorf("snapshot %s has an empty service image label", prev.Key) return fmt.Errorf("snapshot %s has an empty service image label", prev.Key)
} }
c.Image = snapshotImage c.Image = snapshotImage
err = updateConfig(c.ID, c.Image)
if err != nil {
return err
}
c.SnapshotKey = prev.Key c.SnapshotKey = prev.Key
return nil return nil
} }
+3 -3
View File
@@ -12,7 +12,7 @@ import (
manifeststore "github.com/docker/cli/cli/manifest/store" manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client" registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/trust" "github.com/docker/cli/cli/trust"
"github.com/docker/cli/internal/containerizedengine" clitypes "github.com/docker/cli/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
notaryclient "github.com/theupdateframework/notary/client" notaryclient "github.com/theupdateframework/notary/client"
) )
@@ -20,7 +20,7 @@ import (
// NotaryClientFuncType defines a function that returns a fake notary client // NotaryClientFuncType defines a function that returns a fake notary client
type NotaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) type NotaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
type clientInfoFuncType func() command.ClientInfo type clientInfoFuncType func() command.ClientInfo
type containerizedEngineFuncType func(string) (containerizedengine.Client, error) type containerizedEngineFuncType func(string) (clitypes.ContainerizedClient, error)
// FakeCli emulates the default DockerCli // FakeCli emulates the default DockerCli
type FakeCli struct { type FakeCli struct {
@@ -172,7 +172,7 @@ func EnableContentTrust(c *FakeCli) {
} }
// NewContainerizedEngineClient returns a containerized engine client // NewContainerizedEngineClient returns a containerized engine client
func (c *FakeCli) NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) { func (c *FakeCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) {
if c.containerizedEngineClientFunc != nil { if c.containerizedEngineClientFunc != nil {
return c.containerizedEngineClientFunc(sockPath) return c.containerizedEngineClientFunc(sockPath)
} }
+127
View File
@@ -0,0 +1,127 @@
package versions
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
registryclient "github.com/docker/cli/cli/registry/client"
clitypes "github.com/docker/cli/types"
"github.com/docker/distribution/reference"
ver "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// defaultRuntimeMetadataDir is the location where the metadata file is stored
defaultRuntimeMetadataDir = "/var/lib/docker-engine"
)
// GetEngineVersions reports the versions of the engine that are available
func GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, registryPrefix, imageName, versionString string) (clitypes.AvailableVersions, error) {
if imageName == "" {
var err error
localMetadata, err := GetCurrentRuntimeMetadata("")
if err != nil {
return clitypes.AvailableVersions{}, err
}
imageName = localMetadata.EngineImage
}
imageRef, err := reference.ParseNormalizedNamed(path.Join(registryPrefix, imageName))
if err != nil {
return clitypes.AvailableVersions{}, err
}
tags, err := registryClient.GetTags(ctx, imageRef)
if err != nil {
return clitypes.AvailableVersions{}, err
}
return parseTags(tags, versionString)
}
func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) {
var ret clitypes.AvailableVersions
currentVer, err := ver.NewVersion(currentVersion)
if err != nil {
return ret, errors.Wrapf(err, "failed to parse existing version %s", currentVersion)
}
downgrades := []clitypes.DockerVersion{}
patches := []clitypes.DockerVersion{}
upgrades := []clitypes.DockerVersion{}
currentSegments := currentVer.Segments()
for _, tag := range tags {
tmp, err := ver.NewVersion(tag)
if err != nil {
logrus.Debugf("Unable to parse %s: %s", tag, err)
continue
}
testVersion := clitypes.DockerVersion{Version: *tmp, Tag: tag}
if testVersion.LessThan(currentVer) {
downgrades = append(downgrades, testVersion)
continue
}
testSegments := testVersion.Segments()
// lib always provides min 3 segments
if testSegments[0] == currentSegments[0] &&
testSegments[1] == currentSegments[1] {
patches = append(patches, testVersion)
} else {
upgrades = append(upgrades, testVersion)
}
}
sort.Slice(downgrades, func(i, j int) bool {
return downgrades[i].Version.LessThan(&downgrades[j].Version)
})
sort.Slice(patches, func(i, j int) bool {
return patches[i].Version.LessThan(&patches[j].Version)
})
sort.Slice(upgrades, func(i, j int) bool {
return upgrades[i].Version.LessThan(&upgrades[j].Version)
})
ret.Downgrades = downgrades
ret.Patches = patches
ret.Upgrades = upgrades
return ret, nil
}
// GetCurrentRuntimeMetadata loads the current daemon runtime metadata information from the local host
func GetCurrentRuntimeMetadata(metadataDir string) (*clitypes.RuntimeMetadata, error) {
if metadataDir == "" {
metadataDir = defaultRuntimeMetadataDir
}
filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json")
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var res clitypes.RuntimeMetadata
err = json.Unmarshal(data, &res)
if err != nil {
return nil, errors.Wrapf(err, "malformed runtime metadata file %s", filename)
}
return &res, nil
}
// WriteRuntimeMetadata stores the metadata on the local system
func WriteRuntimeMetadata(metadataDir string, metadata *clitypes.RuntimeMetadata) error {
if metadataDir == "" {
metadataDir = defaultRuntimeMetadataDir
}
filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json")
data, err := json.Marshal(metadata)
if err != nil {
return err
}
os.Remove(filename)
return ioutil.WriteFile(filename, data, 0644)
}
@@ -1,22 +1,15 @@
package containerizedengine package versions
import ( import (
"context" "io/ioutil"
"os"
"path/filepath"
"testing" "testing"
clitypes "github.com/docker/cli/types"
"gotest.tools/assert" "gotest.tools/assert"
) )
func TestGetEngineVersionsBadImage(t *testing.T) {
ctx := context.Background()
client := baseClient{}
currentVersion := "currentversiongoeshere"
imageName := "this is an illegal image $%^&"
_, err := client.GetEngineVersions(ctx, nil, currentVersion, imageName)
assert.ErrorContains(t, err, "invalid reference format")
}
func TestParseTagsSimple(t *testing.T) { func TestParseTagsSimple(t *testing.T) {
tags := []string{"1.0.0", "1.1.2", "1.1.1", "1.2.2"} tags := []string{"1.0.0", "1.1.2", "1.1.1", "1.2.2"}
currentVersion := "1.1.0" currentVersion := "1.1.0"
@@ -78,3 +71,35 @@ func TestParseBadCurrent2(t *testing.T) {
_, err := parseTags(tags, currentVersion) _, err := parseTags(tags, currentVersion)
assert.ErrorContains(t, err, "failed to parse existing") assert.ErrorContains(t, err, "failed to parse existing")
} }
func TestGetCurrentRuntimeMetadataNotPresent(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "docker-root")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
_, err = GetCurrentRuntimeMetadata(tmpdir)
assert.ErrorType(t, err, os.IsNotExist)
}
func TestGetCurrentRuntimeMetadataBadJson(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "docker-root")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
filename := filepath.Join(tmpdir, clitypes.RuntimeMetadataName+".json")
err = ioutil.WriteFile(filename, []byte("not json"), 0644)
assert.NilError(t, err)
_, err = GetCurrentRuntimeMetadata(tmpdir)
assert.ErrorContains(t, err, "malformed runtime metadata file")
}
func TestGetCurrentRuntimeMetadataHappyPath(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "docker-root")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
metadata := clitypes.RuntimeMetadata{Platform: "platformgoeshere"}
err = WriteRuntimeMetadata(tmpdir, &metadata)
assert.NilError(t, err)
res, err := GetCurrentRuntimeMetadata(tmpdir)
assert.NilError(t, err)
assert.Equal(t, res.Platform, "platformgoeshere")
}
+1 -1
View File
@@ -25,7 +25,7 @@ func generateManPages(opts *options) error {
} }
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false) dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil)
cmd := &cobra.Command{Use: "docker"} cmd := &cobra.Command{Use: "docker"}
commands.AddCommands(cmd, dockerCli) commands.AddCommands(cmd, dockerCli)
source := filepath.Join(opts.source, descriptionSourcePath) source := filepath.Join(opts.source, descriptionSourcePath)
+8
View File
@@ -38,5 +38,13 @@ This example sets the docker image ENV variable DEBUG to true by default.
# tar -c . | docker image import -c="ENV DEBUG true" - exampleimagedir # tar -c . | docker image import -c="ENV DEBUG true" - exampleimagedir
## When the daemon supports multiple operating systems
If the daemon supports multiple operating systems, and the image being imported
does not match the default operating system, it may be necessary to add
`--platform`. This would be necessary when importing a Linux image into a Windows
daemon.
# docker image import --platform=linux .\linuximage.tar
# See also # See also
**docker-export(1)** to export the contents of a filesystem as a tar archive to STDOUT. **docker-export(1)** to export the contents of a filesystem as a tar archive to STDOUT.
+1 -1
View File
@@ -48,7 +48,7 @@ repository with "version1.0.test":
## Tagging an image for a private repository ## Tagging an image for a private repository
To push an image to a private registry and not the central Docker Before pushing an image to a private registry and not the central Docker
registry you must tag it with the registry hostname and port (if needed). registry you must tag it with the registry hostname and port (if needed).
docker image tag 0e5574283393 myregistryhost:5000/fedora/httpd:version1.0 docker image tag 0e5574283393 myregistryhost:5000/fedora/httpd:version1.0

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